(*****************************************************************************)
(*                                                                           *)
(* Open Source License                                                       *)
(* Copyright (c) 2018 Dynamic Ledger Solutions, Inc. <contact@tezos.com>     *)
(*                                                                           *)
(* Permission is hereby granted, free of charge, to any person obtaining a   *)
(* copy of this software and associated documentation files (the "Software"),*)
(* to deal in the Software without restriction, including without limitation *)
(* the rights to use, copy, modify, merge, publish, distribute, sublicense,  *)
(* and/or sell copies of the Software, and to permit persons to whom the     *)
(* Software is furnished to do so, subject to the following conditions:      *)
(*                                                                           *)
(* The above copyright notice and this permission notice shall be included   *)
(* in all copies or substantial portions of the Software.                    *)
(*                                                                           *)
(* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR*)
(* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,  *)
(* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL   *)
(* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER*)
(* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING   *)
(* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER       *)
(* DEALINGS IN THE SOFTWARE.                                                 *)
(*                                                                           *)
(*****************************************************************************)

let rec read_partial_context context path depth =
  (* non tail-recursive *)
  if depth = 0 then
    Lwt.return Block_services.Cut
  else
    (* try to read as file *)
    Context.get context path >>= function
    | Some v ->
        Lwt.return (Block_services.Key v)
    | None ->
        (* try to read as directory *)
        Context.fold context path ~init:[] ~f: begin fun k acc ->
          match k with
          | `Key k | `Dir k ->
              read_partial_context context k (depth-1) >>= fun v ->
              let k = List.nth k ((List.length k)-1) in
              Lwt.return ((k,v)::acc)
        end >>= fun l ->
        Lwt.return (Block_services.Dir (List.rev l))

let build_raw_rpc_directory
    (module Proto : Block_services.PROTO)
    (module Next_proto : Registered_protocol.T) =

  let dir : State.Block.t RPC_directory.t ref =
    ref RPC_directory.empty in

  let register0 s f =
    dir :=
      RPC_directory.register !dir (RPC_service.subst0 s)
        (fun block p q -> f block p q) in
  let register1 s f =
    dir :=
      RPC_directory.register !dir (RPC_service.subst1 s)
        (fun (block, a) p q -> f block a p q) in
  let register2 s f =
    dir :=
      RPC_directory.register !dir (RPC_service.subst2 s)
        (fun ((block, a), b) p q -> f block a b p q) in

  let module Block_services = Block_services.Make(Proto)(Next_proto) in
  let module S = Block_services.S in

  register0 S.hash begin fun block () () ->
    return (State.Block.hash block)
  end ;

  register0 S.live_blocks begin fun block () () ->
    Chain_traversal.live_blocks
      block
      (State.Block.max_operations_ttl block)
    >>= fun (live_blocks, _) ->
    return live_blocks
  end ;

  (* block header *)

  register0 S.header begin fun block () () ->
    let chain_id = State.Block.chain_id block in
    let hash = State.Block.hash block in
    let header = State.Block.header block in
    let protocol_data =
      Data_encoding.Binary.of_bytes_exn
        Proto.block_header_data_encoding
        header.protocol_data in
    return { Block_services.hash ; chain_id ;
             shell = header.shell ; protocol_data }
  end ;
  register0 S.raw_header begin fun block () () ->
    let header = State.Block.header block in
    return (Data_encoding.Binary.to_bytes_exn Block_header.encoding header)
  end ;
  register0 S.Header.shell_header begin fun block () () ->
    return (State.Block.header block).shell
  end ;
  register0 S.Header.protocol_data begin fun block () () ->
    let header = State.Block.header block in
    return
      (Data_encoding.Binary.of_bytes_exn
         Proto.block_header_data_encoding
         header.protocol_data)
  end ;
  register0 S.Header.raw_protocol_data begin fun block () () ->
    let header = State.Block.header block in
    return header.protocol_data
  end ;

  (* block metadata *)

  let metadata block =
    let protocol_data =
      Data_encoding.Binary.of_bytes_exn
        Proto.block_header_metadata_encoding
        (State.Block.metadata block) in
    State.Block.test_chain block >>= fun test_chain_status ->
    return {
      Block_services.protocol_data ;
      test_chain_status ;
      max_operations_ttl = State.Block.max_operations_ttl block ;
      max_operation_data_length = Next_proto.max_operation_data_length ;
      max_block_header_length = Next_proto.max_block_length ;
      operation_list_quota =
        List.map
          (fun { Tezos_protocol_environment_shell.max_size; max_op } ->
             { Tezos_shell_services.Block_services.max_size ; max_op } )
          Next_proto.validation_passes ;
    } in

  register0 S.metadata begin fun block () () ->
    metadata block
  end ;

  (* operations *)

  let convert chain_id (op : Operation.t) metadata : Block_services.operation =
    let protocol_data =
      Data_encoding.Binary.of_bytes_exn
        Proto.operation_data_encoding
        op.proto in
    let receipt =
      Data_encoding.Binary.of_bytes_exn
        Proto.operation_receipt_encoding
        metadata in
    { Block_services.chain_id ;
      hash = Operation.hash op ;
      shell = op.shell ;
      protocol_data ;
      receipt ;
    } in

  let operations block =
    State.Block.all_operations block >>= fun ops ->
    State.Block.all_operations_metadata block >>= fun metadata ->
    let chain_id = State.Block.chain_id block in
    return (List.map2 (List.map2 (convert chain_id)) ops metadata) in

  register0 S.Operations.operations begin fun block () () ->
    operations block
  end ;

  register1 S.Operations.operations_in_pass begin fun block i () () ->
    let chain_id = State.Block.chain_id block in
    try
      State.Block.operations block i >>= fun (ops, _path) ->
      State.Block.operations_metadata block i >>= fun metadata ->
      return (List.map2 (convert chain_id) ops metadata)
    with _ -> Lwt.fail Not_found
  end ;

  register2 S.Operations.operation begin fun block i j () () ->
    let chain_id = State.Block.chain_id block in
    begin try
        State.Block.operations block i >>= fun (ops, _path) ->
        State.Block.operations_metadata block i >>= fun metadata ->
        Lwt.return (List.nth ops j, List.nth metadata j)
      with _ -> Lwt.fail Not_found end >>= fun (op, md) ->
    return (convert chain_id op md)
  end ;

  (* operation_hashes *)

  register0 S.Operation_hashes.operation_hashes begin fun block () () ->
    State.Block.all_operation_hashes block >>= return
  end ;

  register1 S.Operation_hashes.operation_hashes_in_pass begin fun block i () () ->
    State.Block.operation_hashes block i >>= fun (ops, _) ->
    return ops
  end ;

  register2 S.Operation_hashes.operation_hash begin fun block i j () () ->
    begin try
        State.Block.operation_hashes block i >>= fun (ops, _) ->
        Lwt.return (List.nth ops j)
      with _ -> Lwt.fail Not_found end >>= fun op ->
    return op
  end ;

  (* context *)

  register1 S.Context.read begin fun block path q () ->
    let depth = Option.unopt ~default:max_int q#depth in
    fail_unless (depth >= 0)
      (Tezos_shell_services.Block_services.Invalid_depth_arg depth) >>=? fun () ->
    State.Block.context block >>= fun context ->
    Context.mem context path >>= fun mem ->
    Context.dir_mem context path >>= fun dir_mem ->
    if not (mem || dir_mem) then
      Lwt.fail Not_found
    else
      read_partial_context context path depth >>= fun dir ->
      return dir
  end ;

  (* info *)

  register0 S.info begin fun block () () ->
    let chain_id = State.Block.chain_id block in
    let hash = State.Block.hash block in
    let header = State.Block.header block in
    let shell = header.shell in
    let protocol_data =
      Data_encoding.Binary.of_bytes_exn
        Proto.block_header_data_encoding
        header.protocol_data in
    metadata block >>=? fun metadata ->
    operations block >>=? fun operations ->
    return { Block_services.hash ; chain_id ;
             header = { shell ; protocol_data } ;
             metadata ; operations }
  end ;

  (* helpers *)

  register0 S.Helpers.Forge.block_header begin fun _block () header ->
    return (Data_encoding.Binary.to_bytes_exn Block_header.encoding header)
  end ;

  register0 S.Helpers.Preapply.block begin fun block q p ->
    let timestamp =
      match q#timestamp with
      | None -> Time.now ()
      | Some time -> time in
    let protocol_data =
      Data_encoding.Binary.to_bytes_exn
        Next_proto.block_header_data_encoding
        p.protocol_data in
    let operations =
      List.map
        (List.map
           (fun op ->
              let proto =
                Data_encoding.Binary.to_bytes_exn
                  Next_proto.operation_data_encoding
                  op.Next_proto.protocol_data in
              { Operation.shell = op.shell ; proto }))
        p.operations in
    Prevalidation.preapply
      ~predecessor:block
      ~timestamp
      ~protocol_data
      operations
  end ;

  register0 S.Helpers.Preapply.operations begin fun block () ops ->
    State.Block.context block >>= fun ctxt ->
    let predecessor = State.Block.hash block in
    let header = State.Block.shell_header block in
    Next_proto.begin_construction
      ~chain_id: (State.Block.chain_id block)
      ~predecessor_context:ctxt
      ~predecessor_timestamp:header.timestamp
      ~predecessor_level:header.level
      ~predecessor_fitness:header.fitness
      ~predecessor
      ~timestamp:(Time.now ()) () >>=? fun state ->
    fold_left_s
      (fun (state, acc) op ->
         Next_proto.apply_operation state op >>=? fun (state, result) ->
         return (state, (op.protocol_data, result) :: acc))
      (state, []) ops >>=? fun (state, acc) ->
    Next_proto.finalize_block state >>=? fun _ ->
    return (List.rev acc)
  end ;

  register1 S.Helpers.complete begin fun block prefix () () ->
    State.Block.context block >>= fun ctxt ->
    Base58.complete prefix >>= fun l1 ->
    Next_proto.complete_b58prefix ctxt prefix >>= fun l2 ->
    return (l1 @ l2)
  end ;

  (* merge protocol rpcs... *)

  RPC_directory.merge
    !dir
    (RPC_directory.map
       (fun block ->
          State.Block.context block >|= fun context ->
          { Tezos_protocol_environment_shell.
            block_hash = State.Block.hash block ;
            block_header = State.Block.shell_header block ;
            context })
       Next_proto.rpc_services)

let get_protocol hash =
  match Registered_protocol.get hash with
  | None -> raise Not_found
  | Some protocol -> protocol

let get_directory block =
  State.Block.get_rpc_directory block >>= function
  | Some dir -> Lwt.return dir
  | None ->
      State.Block.protocol_hash block >>= fun next_protocol_hash ->
      let next_protocol = get_protocol next_protocol_hash in
      State.Block.predecessor block >>= function
      | None ->
          Lwt.return (build_raw_rpc_directory
                        (module Block_services.Fake_protocol)
                        next_protocol)
      | Some pred ->
          State.Block.protocol_hash pred >>= fun protocol_hash ->
          let (module Proto) = get_protocol protocol_hash in
          State.Block.get_rpc_directory block >>= function
          | Some dir -> Lwt.return dir
          | None ->
              let dir = build_raw_rpc_directory (module Proto) next_protocol in
              State.Block.set_rpc_directory block dir >>= fun () ->
              Lwt.return dir

let get_block chain_state = function
  | `Genesis ->
      Chain.genesis chain_state
  |  `Head n ->
      Chain.head chain_state >>= fun head ->
      if n < 0 then
        Lwt.fail Not_found
      else if n = 0 then
        Lwt.return head
      else
        State.Block.read_exn chain_state ~pred:n (State.Block.hash head)
  | `Hash (hash, n) ->
      if n < 0 then
        State.Block.read_exn chain_state hash >>= fun block ->
        Chain.head chain_state >>= fun head ->
        let head_level = State.Block.level head in
        let block_level = State.Block.level block in
        let target =
          Int32.(to_int (sub head_level (sub block_level (of_int n)))) in
        if target < 0 then
          Lwt.fail Not_found
        else
          State.Block.read_exn chain_state ~pred:target (State.Block.hash head)
      else
        State.Block.read_exn chain_state ~pred:n hash
  | `Level i ->
      Chain.head chain_state >>= fun head ->
      let target = Int32.(to_int (sub (State.Block.level head) i)) in
      if target < 0 then
        Lwt.fail Not_found
      else
        State.Block.read_exn chain_state ~pred:target (State.Block.hash head)

let build_rpc_directory chain_state block =
  get_block chain_state block >>= fun block ->
  get_directory block >>= fun dir ->
  Lwt.return (RPC_directory.map (fun _ -> Lwt.return block) dir)

