(*
  This file is part of scilla.

  Copyright (c) 2018 - present Zilliqa Research Pvt. Ltd.

  scilla is free software: you can redistribute it and/or modify it under the
  terms of the GNU General Public License as published by the Free Software
  Foundation, either version 3 of the License, or (at your option) any later
  version.

  scilla is distributed in the hope that it will be useful, but WITHOUT ANY
  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
  A PARTICULAR PURPOSE.  See the GNU General Public License for more details.

  You should have received a copy of the GNU General Public License along with
  scilla.  If not, see <http://www.gnu.org/licenses/>.
*)

open Core
open Scilla_base
open Scilla_eval
open TypeUtil
open StateIPCIdl
open OUnit2
open IPCUtil
module Type = TypeUtil.TUType
module M = Idl.IdM
module IDL = Idl.Make (M)
module IPCClient = IPCIdl (IDL.GenClient ())

(* The purpose of this Test Client is to initialize the test server with data during testing.
 * While ideally we would've liked to reuse StateService, we cannot do so because that deals
 * with Scilla literals, which we cannot always parse from the JSONs. Parsing it requires the
 * definitions of custom ADTs that are only available in the source file. *)

type state_info = (string * Type.t) list

(* For persistance b/w multiple queries. *)
let stateref = ref None

(* Sets up state_info. Should be called before any queries. *)
let initialize ~sock_addr ~(fields : state_info) =
  stateref := Some (sock_addr, fields)

let assert_init () =
  match !stateref with
  | None -> assert_failure "StateIPCTestClient: Uninitialized"
  | Some (sock_addr, fields) -> (sock_addr, fields)

let encode_serialized_value value =
  let encoder = Pbrt.Encoder.create () in
  Ipcmessage_pb.encode_proto_scilla_val value encoder;
  Bytes.to_string @@ Pbrt.Encoder.to_bytes encoder

let decode_serialized_value value =
  let decoder = Pbrt.Decoder.of_bytes value in
  Ipcmessage_pb.decode_proto_scilla_val decoder

let encode_serialized_query query =
  let encoder = Pbrt.Encoder.create () in
  Ipcmessage_pb.encode_proto_scilla_query query encoder;
  Bytes.to_string @@ Pbrt.Encoder.to_bytes encoder

(* Translate JRPC result to our result. *)
let translate_res res =
  match res |> IDL.T.get |> M.run with
  | Error (e : RPCError.err_t) ->
      assert_failure
        (Printf.sprintf
           "StateIPCTestClient: Error in IPC access: (code:%d, message:%s)."
           e.code e.message)
  | Ok res' -> res'

let ipcclient_exn_wrapper thunk =
  try thunk () with
  | Core_unix.Unix_error (_, s1, s2) ->
      assert_failure ("StateIPCTestClient: Unix error: " ^ s1 ^ s2)
  | _ ->
      assert_failure "StateIPCTestClient: Unexpected error making JSON-RPC call"

let binary_rpc ~sock_addr (call : Rpc.call) : Rpc.response M.t =
  let socket =
    Core_unix.socket ~domain:Core_unix.PF_UNIX ~kind:Core_unix.SOCK_STREAM
      ~protocol:0 ()
  in
  Core_unix.connect socket ~addr:(Core_unix.ADDR_UNIX sock_addr);
  let ic, oc =
    (Core_unix.in_channel_of_descr socket, Core_unix.out_channel_of_descr socket)
  in
  let msg_buf = Jsonrpc.string_of_call ~version:Jsonrpc.V2 call in
  (* Cannot use DebugMessage here as we're part of testsuite, and don't want to
   * have races with the main executable that also (may) logs to the same files. *)
  (* (Printf.printf "StateIPCTestClient: Sending: %s\n" msg_buf); *)
  (* Send data to the socket. *)
  let _ = IPCUtil.send_delimited oc msg_buf in
  (* Get response. *)
  let response = Caml.input_line ic in
  Core_unix.close socket;
  (* (Printf.printf "StateIPCTestClient: Response: %s\n" response); *)
  M.return @@ Jsonrpc.response_of_string response

(* Fetch full state variable from server (no indexing). *)
let fetch ~fname =
  let sock_addr, fields = assert_init () in
  let tp =
    match List.Assoc.find fields ~equal:String.( = ) fname with
    | Some tp -> tp
    | None ->
        assert_failure ("StateIPCTestClient: Unable to find field " ^ fname)
  in
  let open Ipcmessage_types in
  let q =
    {
      name = fname;
      mapdepth = TypeUtilities.map_depth tp;
      indices = [];
      ignoreval = false;
    }
  in
  let q' = encode_serialized_query q in
  let res =
    translate_res @@ IPCClient.fetch_state_value (binary_rpc ~sock_addr) q'
  in
  match res with
  | true, res' -> decode_serialized_value (Bytes.of_string res')
  | false, _ ->
      assert_failure
        ("StateIPCTestClient: Field " ^ fname ^ " not found on server")

(* Update full state variable to server (no indexing). *)
let update ~fname ~value =
  let open Ipcmessage_types in
  let sock_addr, fields = assert_init () in
  let tp =
    match List.Assoc.find fields fname ~equal:String.( = ) with
    | Some tp -> tp
    | None ->
        assert_failure ("StateIPCTestClient: Unable to find field " ^ fname)
  in
  let q =
    {
      name = fname;
      mapdepth = TypeUtilities.map_depth tp;
      indices = [];
      ignoreval = false;
    }
  in
  let q' = encode_serialized_query q in
  let value' = encode_serialized_value value in
  translate_res
  @@ IPCClient.update_state_value (binary_rpc ~sock_addr) q' value'

(* Update full state variable (of another contract) to server (no indexing). *)
let update_ext ~caddr ~fname ~value ~tp =
  let open Ipcmessage_types in
  let sock_addr, _ = assert_init () in
  let q =
    {
      name = fname;
      mapdepth = TypeUtilities.map_depth tp;
      indices = [];
      ignoreval = false;
    }
  in
  let q' = encode_serialized_query q in
  let value' = encode_serialized_value value in
  translate_res
  @@ IPCClient.set_ext_state_value (binary_rpc ~sock_addr) caddr q' value'
       (Type.pp_typ tp)

let fetch_all () =
  let _, fields = assert_init () in
  List.map fields ~f:(fun (fname, tp) -> (fname, tp, fetch ~fname))

(* Fetch blockchain info. The semantics and format of 
 * ~query_args and the result depends on ~query_name 
 * Any error on the blockchain side or IPC is forwarded
 * (via the error monad). *)
let fetch_bcinfo ~query_name ~query_args =
  let sock_addr, _ = assert_init () in
  let res =
    translate_res
    @@ IPCClient.fetch_bcinfo (binary_rpc ~sock_addr) query_name query_args
  in
  match res with
  | true, res' -> res'
  | false, _ -> assert_failure "Error fetching blockchain info"

let set_bcinfo ~query_name ~query_args value =
  let sock_addr, _ = assert_init () in
  translate_res
  @@ IPCClient.set_bcinfo (binary_rpc ~sock_addr) query_name query_args value
