(**************************************************************************)
(*  ocplib-resto                                                          *)
(*  Copyright (C) 2016, OCamlPro.                                         *)
(*                                                                        *)
(*    All rights reserved.  This file is distributed under the terms      *)
(*    of the GNU Lesser General Public License version 2.1, with the      *)
(*    special exception on linking described in the file LICENSE.         *)
(*                                                                        *)
(**************************************************************************)

type meth = [ `GET | `POST | `DELETE | `PUT | `PATCH ]

let string_of_meth = function
  | `GET -> "GET"
  | `POST -> "POST"
  | `DELETE -> "DELETE"
  | `PUT -> "PUT"
  | `PATCH -> "PATCH"

let meth_of_string = function
  | "GET" -> Some `GET
  | "POST" -> Some `POST
  | "DELETE" -> Some `DELETE
  | "PUT" -> Some `PUT
  | "PATCH" -> Some `PATCH
  | _ -> None

module MethMap = Map.Make(struct type t = meth let compare = compare end)
module StringMap = Map.Make(String)

type (_, _) eq = Eq : ('a, 'a) eq

module Internal = struct

  module Ty = struct

    type 'a witness = ..
    exception Not_equal
    module type Ty = sig
      type t val witness : t witness
      val eq: 'a witness -> ('a, t) eq
    end
    type 'a id = (module Ty with type t = 'a)
    let new_id (type a) () =
      let module Ty = struct
        type t = a
        type 'a witness += Ty : t witness
        let witness = Ty
        let eq (type b) : b witness -> (b, t) eq =
          function Ty -> Eq | _ -> raise Not_equal
      end in
      (module Ty : Ty with type t = a)
    let eq : type a b. a id -> b id -> (a, b) eq =
      fun (module TyA) (module TyB) ->  TyB.eq TyA.witness

  end

  type descr = {
    name: string ;
    descr: string option ;
  }

  type 'a arg = {
    id: 'a Ty.id;
    destruct: string -> ('a, string) result ;
    construct: 'a -> string ;
    descr: descr ;
  }

  let from_arg x = x
  let to_arg x = x

  type (_,_) path =
    | Root : ('rkey, 'rkey) path
    | Static : ('rkey, 'key) path * string -> ('rkey, 'key) path
    | Dynamic : ('rkey, 'key) path * 'a arg -> ('rkey, 'key * 'a) path
    | DynamicTail : ('rkey, 'key) path * 'a arg -> ('rkey, 'key * 'a list) path

  let rec subst0 : type a b. (a, a) path -> (b, b) path = function
    | Root -> Root
    | Static (path, name) -> Static (subst0 path, name)
    | Dynamic (path, arg) -> assert false (* impossible *)
    | DynamicTail (path, arg) -> assert false (* impossible *)

  let rec subst1 : type a b c. (a, a * c) path -> (b, b * c) path = function
    | Root -> assert false (* impossible *)
    | Static (path, name) -> Static (subst1 path, name)
    | Dynamic (path, arg) -> Dynamic (subst0 path, arg)
    | DynamicTail (path, arg) -> DynamicTail (subst0 path, arg)

  let rec subst2 : type a b c d. (a, (a * c) * d) path -> (b, (b * c) * d) path = function
    | Root -> assert false (* impossible *)
    | Static (path, name) -> Static (subst2 path, name)
    | Dynamic (path, arg) -> Dynamic (subst1 path, arg)
    | DynamicTail (path, arg) -> DynamicTail (subst1 path, arg)

  let rec subst3 : type a b c d e. (a, ((a * c) * d) * e) path -> (b, ((b * c) * d) * e) path = function
    | Root -> assert false (* impossible *)
    | Static (path, name) -> Static (subst3 path, name)
    | Dynamic (path, arg) -> Dynamic (subst2 path, arg)
    | DynamicTail (path, arg) -> DynamicTail (subst2 path, arg)

  let from_path x = x
  let to_path x = x

  type 'a query =
    (* inspired from Irmin.Ty.record. *)
    | Fields: ('a, 'b) query_fields * 'b -> 'a query

  and ('a, 'b) query_fields =
    | F0: ('a, 'a) query_fields
    | F1: ('a, 'b) query_field * ('a, 'c) query_fields ->
      ('a, 'b -> 'c) query_fields

  and ('a, 'b) query_field =
    | Single : {
        name : string ; description : string option ;
        ty : 'b arg ; default : 'b ; get : 'a -> 'b ;
      } -> ('a, 'b) query_field
    | Opt : {
        name : string ; description : string option ;
        ty : 'b arg ; get : 'a -> 'b option ;
      } -> ('a, 'b option) query_field
    | Flag : {
        name : string ; description : string option ;
        get : 'a -> bool ;
      } -> ('a, bool) query_field
    | Multi : {
        name : string ; description : string option ;
        ty : 'b arg ; get : 'a -> 'b list ;
      } -> ('a, 'b list) query_field

  type query_kind =
    | Single of descr
    | Optional of descr
    | Flag
    | Multi of descr

  let field_name (type t) : (_,t) query_field -> _ = function
    | Single { name } -> name
    | Opt { name } -> name
    | Flag { name } -> name
    | Multi { name } -> name
  let field_description (type t) : (_,t) query_field -> _ = function
    | Single { description } -> description
    | Opt { description } -> description
    | Flag { description } -> description
    | Multi { description } -> description
  let field_kind (type t) : (_,t) query_field -> query_kind = function
    | Single { ty ; _ } -> Single ty.descr
    | Opt { ty ; _ } -> Optional ty.descr
    | Flag _ -> Flag
    | Multi { ty ; _ } -> Multi ty.descr

  let from_query x = x
  let to_query x = x

end

open Internal

module Arg = struct

  type descr = Internal.descr = {
    name: string ;
    descr: string option ;
  }
  type 'a t = 'a Internal.arg
  type 'a arg = 'a t

  let make ?descr ~name ~destruct ~construct () =
    let id = Ty.new_id () in
    let descr = { name ; descr } in
    { descr ; id ; construct ; destruct }

  let like arg ?descr name =
    { arg with id = Ty.new_id () ; descr = { name ; descr } }

  let descr (ty: 'a arg) = ty.descr

  let ignore : unit arg =
    let destruct _ = Ok () in
    let construct () = "" in
    make ~name:"unit" ~destruct ~construct ()
  let bool : bool arg =
    let bool_of_string s =
      match String.lowercase_ascii s with
      | "false" | "no" -> Ok false
      | _ -> Ok true in
    let string_of_bool = function
      | true -> "yes"
      | false -> "no" in
    make ~name:"bool" ~destruct:bool_of_string ~construct:string_of_bool ()
  let int =
    let int_of_string s =
      try Ok (int_of_string s)
      with Failure _ ->
        Error (Printf.sprintf "Cannot parse integer value: %S." s) in
    make ~name:"int" ~destruct:int_of_string ~construct:string_of_int ()
  let float =
    let float_of_string s =
      try Ok (float_of_string s)
      with Failure _ ->
        Error (Printf.sprintf "Cannot parse float value: %S." s) in
    make ~name:"float" ~destruct:float_of_string ~construct:string_of_float ()
  let int32 =
    let int32_of_string s =
      try Ok (Int32.of_string s)
      with Failure _ ->
        Error (Printf.sprintf "Cannot parse int32 value: %S." s) in
    make ~name:"int32" ~destruct:int32_of_string ~construct:Int32.to_string ()
  let int64 =
    let int64_of_string s =
      try Ok (Int64.of_string s)
      with Failure _ ->
        Error (Printf.sprintf "Cannot parse int64 value: %S." s) in
    make ~name:"int64" ~destruct:int64_of_string ~construct:Int64.to_string ()
  let string =
    make ~name:"string" ~destruct:(fun x -> Ok x) ~construct:(fun x -> x) ()

  let eq a1 a2 =
    try Some (Ty.eq a1.id a2.id)
    with Internal.Ty.Not_equal -> None

end

module Path = struct

  type ('a, 'b) t = ('a, 'b) Internal.path
  type ('a, 'b) path = ('a, 'b) Internal.path

  type 'prefix context = ('prefix, 'prefix) path

  let root = Root
  let open_root = Root

  let add_suffix (type p pr) (path : (p, pr) path) name =
    match path with
    | DynamicTail _ -> invalid_arg "Resto.Path.add_suffix"
    | path -> Static (path, name)

  let add_arg (type p pr) (path : (p, pr) path)  arg =
    match path with
    | DynamicTail _ -> invalid_arg "Resto.Path.add_arg"
    | path -> Dynamic (path, arg)

  let add_final_args (type p pr) (path : (p, pr) path)  arg =
    match path with
    | DynamicTail _ -> invalid_arg "Resto.Path.add_final_arg"
    | path -> DynamicTail (path, arg)

  let prefix
    : type p pr a. (pr, a) path -> (a, p) path -> (pr, p) path
    = fun p1 p2 ->
      let rec prefix
        : type pr a k.
          (pr, a) path -> (a, k) path -> (pr, k) path
        = fun p1 p2 ->
          match p2 with
          | Root -> p1
          | Static (path, name) -> add_suffix (prefix p1 path) name
          | Dynamic (path, arg) -> add_arg (prefix p1 path) arg
          | DynamicTail (path, arg) -> add_final_args (prefix p1 path) arg
      in
      match p1 with
      | DynamicTail _ -> invalid_arg "Resto.Path.prefix"
      | _ -> prefix p1 p2

  let (/) = add_suffix
  let (/:) = add_arg
  let (/:*) = add_final_args

  let subst0 = Internal.subst0
  let subst1 = Internal.subst1
  let subst2 = Internal.subst2
  let subst3 = Internal.subst3

end

module Query = struct

  type 'a t = 'a Internal.query
  type 'a query = 'a Internal.query
  type ('a, 'b) field = ('a, 'b) Internal.query_field

  type ('a, 'b, 'c) open_query =
    ('a, 'c) query_fields -> 'b * ('a, 'b) query_fields

  let field ?descr name ty default get : (_,_) query_field =
    Single { name; description = descr ; ty ; default ; get }

  let opt_field ?descr name ty get : (_,_) query_field =
    Opt { name; description = descr ; ty ; get }

  let flag ?descr name get : (_,_) query_field =
    Flag { name; description = descr ; get }

  let multi_field ?descr name ty get : (_,_) query_field =
    Multi { name; description = descr ; ty ; get }

  let query : 'b -> ('a, 'b, 'b) open_query =
    fun c fs -> c, fs

  let app : type a b c d.
    (a, b, c -> d) open_query -> (a, c) query_field -> (a, b, d) open_query
    = fun r f fs ->
      let c, fs = r (F1 (f, fs)) in
      c, fs

  let seal : type a b. (a, b, a) open_query -> a t =
    fun r ->
      let c, fs = r F0 in
      Fields (fs, c)

  let (|+) = app

  let empty = Fields (F0 , ())

  type 'a efield = Field: ('a, 'b) query_field -> 'a efield
  let fold_fields (type fs) ~f ~init fs =
    let rec loop : type f. _ -> (fs, f) query_fields -> _ = fun acc -> function
      | F0 -> acc
      | F1 (field, fs) -> loop (f acc (Field field)) fs in
    loop init fs

  type 'a parsed_field =
    | Parsed: ('a, 'b) query_field * 'b option -> 'a parsed_field

  let rec rebuild
    : type fs f. _ -> (fs, f) query_fields -> f -> fs
    = fun map fs f ->
      match fs with
      | F0 -> f
      | F1 (Single field, fs) -> begin
          match StringMap.find field.name map with
          | Parsed (Single field', v) ->
              let Eq = Ty.eq field.ty.id field'.ty.id in
              let v = match v with None -> field.default | Some v -> v in
              rebuild map fs (f v)
          | Parsed _ -> assert false
        end
      | F1 (Opt field, fs) -> begin
          match StringMap.find field.name map with
          | Parsed (Opt field', v) ->
              let Eq = Ty.eq field.ty.id field'.ty.id in
              let v = match v with None -> None | Some v -> v in
              rebuild map fs (f v)
          | Parsed _ -> assert false
        end
      | F1 (Flag field, fs) -> begin
          match StringMap.find field.name map with
          | Parsed (Flag _, v) ->
              let v = match v with None -> false | Some v -> v in
              rebuild map fs (f v)
          | Parsed _ -> assert false
        end
      | F1 (Multi field, fs) -> begin
          match StringMap.find field.name map with
          | Parsed (Multi field', v) ->
              let Eq = Ty.eq field.ty.id field'.ty.id in
              let v = match v with None -> [] | Some v -> v in
              rebuild map fs (f v)
          | Parsed _ -> assert false
        end

  exception Invalid of string
  type untyped = (string * string) list
  let parse (Fields (fs, f)) =
    let fields =
      fold_fields
        ~f:(fun map (Field f) ->
            StringMap.add (field_name f) (Parsed (f, None)) map)
        ~init:StringMap.empty
        fs in
    fun query ->
      let fail fmt = Format.kasprintf (fun s -> raise (Invalid s)) fmt in
      let fields =
        List.fold_left
          begin fun fields (name, value) ->
            match StringMap.find name fields with
            | exception Not_found -> fields
            | (Parsed (Single f, Some _)) ->
                fail "Duplicate argument '%s' in query string." name
            | (Parsed (Opt f, Some _)) ->
                fail "Duplicate argument '%s' in query string." name
            | (Parsed (Flag f, Some _)) ->
                fail "Duplicate argument '%s' in query string." name
            | (Parsed (Single f, None)) -> begin
                match f.ty.destruct value with
                | Error error ->
                    fail "Failed to parse argument '%s' (%S): %s"
                      name value error
                | Ok v -> StringMap.add name (Parsed (Single f, Some v)) fields
              end
            | (Parsed (Opt f, None)) -> begin
                match f.ty.destruct value with
                | Error error ->
                    fail "Failed to parse argument '%s' (%S): %s"
                      name value error
                | Ok v -> StringMap.add name (Parsed (Opt f, Some (Some v))) fields
              end
            | (Parsed (Flag f, None)) -> begin
                let v =
                  match String.lowercase_ascii value with
                  | "no" | "false" -> false
                  | _ -> true
                in
                StringMap.add name (Parsed (Flag f, Some v)) fields
              end
            | (Parsed (Multi f, previous)) -> begin
                match f.ty.destruct value with
                | Error error ->
                    fail "Failed to parse argument '%s' (%S): %s"
                      name value error
                | Ok v ->
                    let v =
                      match previous with
                      | None -> [v]
                      | Some l -> v :: l in
                    StringMap.add name (Parsed (Multi f, Some v)) fields
              end
          end
          fields query in
      rebuild fields fs f

end

module Description = struct

  type request = {
    recurse: bool ;
  }

  let request_query =
    let open Query in
    query (fun recurse -> { recurse })
    |+ field "recurse" Arg.bool false (fun t -> t.recurse)
    |> seal

  type nonrec query_kind = query_kind =
    | Single of Arg.descr
    | Optional of Arg.descr
    | Flag
    | Multi of Arg.descr

  type 'schema service = {
    description: string option ;
    path: path_item list ;
    meth: meth ;
    query: query_item list ;
    input: 'schema option ;
    output: 'schema ;
    error: 'schema ;
  }

  and path_item =
    | PStatic of string
    | PDynamic of Arg.descr
    | PDynamicTail of Arg.descr

  and query_item = {
    name: string ;
    description: string option ;
    kind: query_kind ;
  }

  type 'schema directory =
    | Empty
    | Static of 'schema static_directory
    | Dynamic of string option

  and 'schema static_directory = {
    services: 'schema service MethMap.t ;
    subdirs: 'schema static_subdirectories option ;
  }

  and 'schema static_subdirectories =
    | Suffixes of 'schema directory Map.Make(String).t
    | Arg of Arg.descr * 'schema directory

  let rec pp_print_directory ppf =
    let open Format in
    function
    | Empty ->
        fprintf ppf "<empty>"
    | Static dir ->
        fprintf ppf "@[%a@]" pp_print_static_directory dir
    | Dynamic None ->
        fprintf ppf "<dyntree>"
    | Dynamic (Some descr) ->
        fprintf ppf "<dyntree> : %s" descr

  and pp_print_static_directory ppf =
    let open Format in
    function
    | { services ; subdirs = None } when MethMap.is_empty services ->
        fprintf ppf "{}"
    | { services ; subdirs = None } ->
        fprintf ppf "@[<v>%a@]"
          pp_print_dispatch_services services
    | { services ; subdirs = Some subdirs } when MethMap.is_empty services ->
        fprintf ppf "%a"
          pp_print_static_subdirectories subdirs
    | { services ; subdirs = Some subdirs } ->
        fprintf ppf "@[<v>%a@ %a@]"
          pp_print_dispatch_services services
          pp_print_static_subdirectories subdirs

  and pp_print_static_subdirectories ppf =
    let open Format in
    function
    | Suffixes map ->
        let print_binding ppf (name, tree) =
          fprintf ppf "@[<hov 2>%s:@ %a@]"
            name pp_print_directory tree in
        fprintf ppf "@[<v>%a@]"
          (pp_print_list ~pp_sep:pp_print_cut print_binding)
          (StringMap.bindings map)
    | Arg (arg, tree) ->
        fprintf ppf "@[<hov 2>[:%s:]@ @[%a@]@]"
          (arg.name) pp_print_directory tree

  and pp_print_dispatch_services ppf services =
    MethMap.iter
      begin fun meth s ->
        match s with
        | { description = None ; meth ; _ } ->
            Format.fprintf ppf "<%s>" (string_of_meth meth)
        | { description = Some descr ; meth ; _ } ->
            Format.fprintf ppf "<%s> : %s" (string_of_meth meth) descr
      end
      services

end

module type ENCODING = sig
  type 'a t
  type schema
  val unit : unit t
  val untyped : string t
  val conv : ('a -> 'b) -> ('b -> 'a) -> 'b t -> 'a t
  val schema : ?definitions_path:string -> 'a t -> schema
  val description_request_encoding : Description.request t
  val description_answer_encoding : schema Description.directory t
end

module MakeService(Encoding : ENCODING) = struct

  module Internal = struct
    include Internal
    type ('query, 'input, 'output, 'error) types = {
      query : 'query query ;
      input : 'input input ;
      output : 'output Encoding.t ;
      error : 'error Encoding.t ;
    }
    and _ input =
      | No_input : unit input
      | Input : 'input Encoding.t -> 'input input
    type (+'meth, 'prefix, 'params, 'query,
          'input, 'output, 'error) iservice = {
      description : string option ;
      meth : 'meth ;
      path : ('prefix, 'params) path ;
      types : ('query, 'input, 'output, 'error) types ;
    } constraint 'meth = [< meth ]
    let from_service x = x
    let to_service x = x

    type (_, _) eq =
      | Eq : (('query, 'input, 'output, 'error) types,
              ('query, 'input, 'output, 'error) types) eq
    exception Not_equal
    let eq :
      type query1 input1 output1 error1  query2 input2 output2 error2.
      (query1, input1, output1, error1) types ->
      (query2, input2, output2, error2) types ->
      ((query1, input1, output1, error1) types,
       (query2, input2, output2, error2) types) eq
      = fun x y ->
        if Obj.magic x == Obj.magic y then
          Obj.magic Eq (* FIXME *)
        else
          raise Not_equal

  end
  include Internal
  open Path

  type (+'meth, 'prefix, 'params, 'query, 'input, 'output, 'error) t =
    ('meth, 'prefix, 'params, 'query, 'input, 'output, 'error) Internal.iservice
  type (+'meth, 'prefix, 'params, 'query, 'input, 'output, 'error) service =
    ('meth, 'prefix, 'params, 'query, 'input, 'output, 'error) t

  let get_service ?description ~query ~output ~error path =
    let input = No_input in
    { meth = `GET ; description ; path ;
      types = { query ; input ; output ; error } }

  let post_service ?description ~query ~input ~output ~error path =
    let input = Input input in
    { meth = `POST ; description ; path ;
      types = { query ; input ; output ; error } }

  let delete_service ?description ~query ~output ~error path =
    let input = No_input in
    { meth = `DELETE ; description ; path ;
      types = { query ; input ; output ; error } }

  let put_service ?description ~query ~input ~output ~error path =
    let input = Input input in
    { meth = `PUT ; description ; path ;
      types = { query ; input ; output ; error } }

  let patch_service ?description ~query ~input ~output ~error path =
    let input = Input input in
    { meth = `PATCH ; description ; path ;
      types = { query ; input ; output ; error } }

  let prefix path s = { s with path = Path.prefix path s.path }
  let subst0 s = { s with path = Internal.subst0 s.path }
  let subst1 s = { s with path = Internal.subst1 s.path }
  let subst2 s = { s with path = Internal.subst2 s.path }
  let subst3 s = { s with path = Internal.subst3 s.path }

  let meth = fun { meth } -> meth

  let query
    : type pr p i q o e.
      (_, pr, p, q, i, o, e) service -> q Query.t
    = fun { types } -> types.query

  let input_encoding
    : type pr p i q o e.
      (_, pr , p, q, i, o, e) service -> i input
    = fun { types } -> types.input

  let output_encoding
    : type pr p i q o e.
      (_, pr, p, q, i, o, e) service -> o Encoding.t
    = fun { types } -> types.output

  let error_encoding
    : type pr p i q o e.
      (_, pr, p, q, i, o, e) service -> e Encoding.t
    = fun { types } -> types.error

  type ('prefix, 'params, 'error) description_service =
    ([ `GET ], 'prefix, 'params * string list, Description.request,
     unit, Encoding.schema Description.directory, 'error) service

  let description_service ?description error path =
    let description =
      match description with
      | Some descr -> descr
      | None -> "<TODO>"
    in
    get_service
      ~description
      ~query:Description.request_query
      ~output:Encoding.description_answer_encoding
      ~error
      Path.(path /:* Arg.string)

  type 'input request = {
    meth: meth ;
    uri: Uri.t ;
    input: 'input input ;
  }

  let forge_request_args
    : type pr p. (pr, p) path -> p -> string list
    = fun path args ->
      let rec forge_request_args
        : type k. (pr, k) path -> k -> string list -> string list
        = fun path args acc ->
          match path, args with
          | Root, _ ->
              acc
          | Static (path, name), args ->
              forge_request_args path args (name :: acc)
          | Dynamic (path, arg), (args, x) ->
              forge_request_args path args (arg.construct x :: acc)
          | DynamicTail (path, arg), (args, xs) ->
              forge_request_args path args
                (List.fold_right (fun x acc -> arg.construct x :: acc) xs acc) in
      forge_request_args path args []

  let forge_request_query
    : type q. q query -> q -> (string * string) list
    = fun (Fields (fields, _)) q ->
      let rec loop : type t. (q, t) query_fields -> _ = function
        | F0 -> []
        | F1 (Single { name ; ty ; get ; _ }, fields) ->
            (name, ty.construct (get q)) :: loop fields
        | F1 (Opt { name ; ty ; get ; _ }, fields) -> begin
            match get q with
            | None -> loop fields
            | Some v -> (name, ty.construct v) :: loop fields
          end
        | F1 (Flag { name ; get ; _ }, fields) -> begin
            match get q with
            | false -> loop fields
            | true -> (name, "true") :: loop fields
          end
        | F1 (Multi { name ; ty ; get ; _ }, fields) -> begin
            match get q with
            | [] -> loop fields
            | l ->
                List.fold_right
                  (fun v acc -> (name, ty.construct v) :: acc)
                  l
                  (loop fields)
          end in
      loop fields

  let forge_partial_request
    : type pr p i q o e.
      (_, pr, p, q, i, o, e) service -> ?base:Uri.t -> p -> q -> i request
    = fun s ?base:(uri = Uri.empty) args query ->
      let path = String.concat "/" (forge_request_args s.path args) in
      let prefix = Uri.path uri in
      let prefixed_path = if prefix = "" then path else prefix ^ "/" ^ path in
      let uri = Uri.with_path uri prefixed_path in
      let uri = Uri.with_query' uri (forge_request_query s.types.query query) in
      { meth = s.meth ; uri ; input = s.types.input }

  let forge_partial_request =
    (forge_partial_request
     : (meth, _, _, _, _, _, _) service -> _
     :> ([< meth], _, _, _, _, _, _) service -> _ )

  let forge_request =
    (forge_partial_request
     : (meth, _, _, _, _, _, _) service -> _
     :> ([< meth], unit, _, _, _, _, _) service -> _ )

end
