open Misc

open Proto_alpha_utils.Error_monad
open Memory_proto_alpha
open Alpha_context

open Script_ir_translator
open Script_typed_ir

module Option = Simple_utils.Option
module Cast = Proto_alpha_utils.Cast

type ('param, 'storage) toplevel = {
  param_type : 'param ty ;
  storage_type : 'storage ty ;
  code : ('param * 'storage, packed_internal_operation list * 'storage) lambda
}

type ex_toplevel =
    Ex_toplevel : ('a, 'b) toplevel -> ex_toplevel

let get_toplevel ?environment toplevel_path claimed_storage_type claimed_parameter_type =
  let toplevel_str = Streams.read_file toplevel_path in
  contextualize ?environment ~msg:"toplevel" @@ fun {tezos_context = context ; _ } ->
  let toplevel_expr = Cast.tl_of_string toplevel_str in
  let (param_ty_node, storage_ty_node, code_field) =
    force_ok_alpha ~msg:"parsing toplevel" @@
    parse_toplevel toplevel_expr in
  let (Ex_ty param_type, _) =
    force_ok_alpha ~msg:"parse arg ty" @@
    Script_ir_translator.parse_ty context ~allow_big_map:false ~allow_operation:false param_ty_node in
  let (Ex_ty storage_type, _) =
    force_ok_alpha ~msg:"parse storage ty" @@
    parse_storage_ty context storage_ty_node in
  let _ = force_ok_alpha ~msg:"storage eq" @@ Script_ir_translator.ty_eq context storage_type claimed_storage_type in
  let _ = force_ok_alpha ~msg:"param eq" @@ Script_ir_translator.ty_eq context param_type claimed_parameter_type in
  let param_type_full = Pair_t ((claimed_parameter_type, None, None),
                                (claimed_storage_type, None, None), None) in
  let ret_type_full =
    Pair_t ((List_t (Operation_t None, None), None, None),
            (claimed_storage_type, None, None), None) in
  parse_returning (Toplevel { storage_type = claimed_storage_type ; param_type = claimed_parameter_type })
    context (param_type_full, None) ret_type_full code_field >>=?? fun (code, _) ->
  Error_monad.return {
    param_type = claimed_parameter_type;
    storage_type = claimed_storage_type;
    code ;
  }

let make_toplevel code storage_type param_type =
  { param_type ; storage_type ; code }

module type ENVIRONMENT = sig
  val identities : identity list
  val tezos_context : t
end

type ex_typed_stack = Ex_typed_stack : ('a stack_ty * 'a Script_interpreter.stack) -> ex_typed_stack

open Error_monad

module Step (Env: ENVIRONMENT) = struct
  open Env

  type config = {
    source : Contract.t option ;
    payer : Contract.t option ;
    self : Contract.t option ;
    visitor : (Script_interpreter.ex_descr_stack -> unit) option ;
    timestamp : Script_timestamp.t option ;
    debug_visitor : (ex_typed_stack -> unit) option ;
    amount : Tez.t option ;
  }

  let no_config = {
    source = None ;
    payer = None ;
    self = None ;
    visitor = None ;
    debug_visitor = None ;
    timestamp = None ;
    amount = None ;
  }

  let of_param base param = match param with
    | None -> base
    | Some _ as x -> x

  let make_config ?base_config ?source ?payer ?self ?visitor ?debug_visitor ?timestamp ?amount () =
    let base_config = Option.unopt ~default:no_config base_config in {
      source = Option.bind_eager_or source base_config.source ;
      payer = Option.bind_eager_or payer base_config.payer ;
      self = Option.bind_eager_or self base_config.self ;
      visitor = Option.bind_eager_or visitor base_config.visitor ;
      debug_visitor = Option.bind_eager_or debug_visitor base_config.debug_visitor ;
      timestamp = Option.bind_eager_or timestamp base_config.timestamp ;
      amount = Option.bind_eager_or amount base_config.amount ;
    }

  open Error_monad

  let debug_visitor ?f () =
    let open Script_interpreter in
    let aux (Ex_descr_stack (descr, stack)) =
      (match (descr.instr, descr.bef) with
       | Nop, Item_t (String_t _, stack_ty, _) -> (
           let (Item (s, stack)) = stack in
           if s = "_debug"
           then (
             match f with
             | None -> Format.printf "debug: %s\n%!" @@ Cast.stack_to_string stack_ty stack
             | Some f -> f (Ex_typed_stack(stack_ty, stack))
           ) else ()
         )
       | _ -> ()) ;
      () in
    aux

  let step_lwt ?(config=no_config) (stack:'a Script_interpreter.stack) (code:('a, 'b) descr) =
    let source = Option.unopt
        ~default:(List.nth identities 0).implicit_contract config.source in
    let payer = Option.unopt
        ~default:(List.nth identities 1).implicit_contract config.payer in
    let self = Option.unopt
        ~default:(List.nth identities 2).implicit_contract config.self in
    let amount = Option.unopt ~default:(Tez.one) config.amount in
    let visitor =
      let default = debug_visitor ?f:config.debug_visitor () in
      Option.unopt ~default config.visitor in
    let tezos_context = match config.timestamp with
      | None -> tezos_context
      | Some s -> Alpha_context.Script_timestamp.set_now tezos_context s in
    Script_interpreter.step tezos_context ~source ~payer ~self ~visitor amount code stack >>=?? fun (stack, _) ->
    return stack

  let step_1_2 ?config (a:'a) (descr:('a * end_of_stack, 'b * ('c * end_of_stack)) descr) =
    let open Script_interpreter in
    step_lwt ?config (Item(a, Empty)) descr >>=? fun (Item(b, Item(c, Empty))) ->
    return (b, c)

  let step_3_1 ?config (a:'a) (b:'b) (c:'c)
      (descr:('a * ('b * ('c * end_of_stack)), 'd * end_of_stack) descr) =
    let open Script_interpreter in
    step_lwt ?config (Item(a, Item(b, Item(c, Empty)))) descr >>=? fun (Item(d, Empty)) ->
    return d

  let step_2_1 ?config (a:'a) (b:'b) (descr:('a * ('b * end_of_stack), 'c * end_of_stack) descr) =
    let open Script_interpreter in
    step_lwt ?config (Item(a, Item(b, Empty))) descr >>=? fun (Item(c, Empty)) ->
    return c

  let step_1_1 ?config (a:'a) (descr:('a * end_of_stack, 'b * end_of_stack) descr) =
    let open Script_interpreter in
    step_lwt ?config (Item(a, Empty)) descr >>=? fun (Item(b, Empty)) ->
    return b

  let step_value ?config (a:'a) (descr:('a * end_of_stack, 'a * end_of_stack) descr) =
    step_1_1 ?config a descr

  let step ?config stack code =
    force_lwt ~msg:"running a step" @@ step_lwt ?config stack code

end

let run_lwt_full ?source ?payer ?self toplevel storage param {identities ; tezos_context = context} =
  let { code ; _ } : (_, _) toplevel = toplevel in

  let source = Option.unopt
      ~default:(List.nth identities 0).implicit_contract source in
  let payer = Option.unopt
      ~default:(List.nth identities 1).implicit_contract payer in
  let self = Option.unopt
      ~default:(List.nth identities 2).implicit_contract self in
  let amount = Tez.one in

  Script_interpreter.interp context ~source ~payer ~self amount code (param, storage)
  >>=?? fun ((ops, storage), new_ctxt) ->
  let gas = Alpha_context.Gas.consumed ~since:context ~until:new_ctxt in
  return (storage, ops, gas)

let run_lwt ?source ?payer ?self toplevel storage param env =
  run_lwt_full ?source ?payer ?self toplevel storage param env >>=? fun (storage, _ops, _gas) ->
  return storage

let run ?environment toplevel storage param =
  contextualize ?environment ~msg:"run toplevel" @@ run_lwt toplevel storage param

let run_node ?environment toplevel storage_node param_node =
  contextualize ?environment ~msg:"run toplevel" @@ fun {tezos_context = context ; _} ->
  let {param_type ; storage_type ; _ } = toplevel in
  parse_data context param_type param_node >>=?? fun (param, _) ->
  parse_data context storage_type storage_node >>=?? fun (storage, _) ->
  let storage = run toplevel storage param in
  unparse_data context Readable storage_type storage >>=?? fun (storage_node, _) ->
  return storage_node

let run_str toplevel storage_str param_str =
  let param_node = Cast.node_of_string param_str in
  let storage_node = Cast.node_of_string storage_str in
  run_node toplevel storage_node param_node

type input = {
  toplevel_path : string ;
  storage : string ;
  parameter : string
}

let parse_json json_str : input =
  let json = force_ok_str ~msg:"main_contract: invalid json" @@ Tezos_utils.Data_encoding.Json.from_string json_str in
  let json = match json with
    | `O json ->  json
    | _ -> raise @@ Failure "main_contract: not recorD"
  in
  let open Json in
  let toplevel_path = force_string ~msg:"main_contract, top_level" @@ List.assoc "top_level" json in
  let parameter = force_string ~msg:"main_contract, param" @@ List.assoc "param" json in
  let storage = force_string ~msg:"main_contract, storage" @@ List.assoc "storage" json in
  { toplevel_path ; storage ; parameter }

let generate_json (storage_node:Script.node) : string =
  let storage_expr = Tezos_micheline.Micheline.strip_locations storage_node in
  let json = Data_encoding.Json.construct Script.expr_encoding storage_expr in
  Format.fprintf Format.str_formatter "%a" Data_encoding.Json.pp json ;
  Format.flush_str_formatter ()

module Types = struct
  open Script_typed_ir

  let union a b = Union_t ((a, None), (b, None), None)
  let assert_union = function
    | Union_t ((a, _), (b, _), _) -> (a, b)
    | _ -> assert false

  let pair a b = Pair_t ((a, None, None), (b, None, None), None)
  let assert_pair = function
    | Pair_t ((a, _, _), ((b, _, _)), _) -> (a, b)
    | _ -> assert false
  let assert_pair_ex ?(msg="assert pair") (Ex_ty ty) = match ty with
    | Pair_t ((a, _, _), ((b, _, _)), _) -> (Ex_ty a, Ex_ty b)
    | _ -> raise (Failure msg)

  let unit = Unit_t None

  let bytes = Bytes_t None
  let bytes_k = Bytes_key None

  let nat = Nat_t None
  let tez = Mutez_t None
  let int = Int_t None
  let nat_k = Nat_key None
  let tez_k = Mutez_key None
  let int_k = Int_key None

  let big_map k v = Big_map_t (k, v, None)

  let signature = Signature_t None
  let operation = Operation_t None

  let bool = Bool_t None

  let mutez = Mutez_t None

  let string = String_t None
  let string_k = String_key None
  let address_k = Address_key None

  let key = Key_t None

  let list a = List_t (a, None)
  let assert_list = function
    | List_t (a, _) -> a
    | _ -> assert false

  let option a = Option_t ((a, None), None, None)
  let contract a = Contract_t (a, None)
  let assert_option = function
    | Option_t ((a, _), _, _) -> a
    | _ -> assert false

  let address = Address_t None

  let lambda a b = Lambda_t (a, b, None)
  let assert_lambda = function
    | Lambda_t (a, b, _) -> (a, b)
    | _ -> assert false
  type ex_lambda = Ex_lambda : (_, _) lambda ty -> ex_lambda
  let is_lambda : type a . a ty -> ex_lambda option = function
    | Lambda_t (_, _, _) as x -> Some (Ex_lambda x)
    | _ -> None

  let timestamp = Timestamp_t None
  let timestamp_k = Timestamp_key None

  let map a b = Map_t (a, b, None)

  let assert_type (_:'a ty) (_:'a) = ()
end

module Values = struct
  let empty_map t = empty_map t

  let empty_big_map key_type comparable_key_ty value_type : ('a, 'b) big_map = {
    key_type ; value_type ; diff = empty_map comparable_key_ty ;
  }

  let int n = Script_int.of_int n

  let nat n = Script_int.abs @@ Script_int.of_int n
  let nat_to_int n = Option.unopt_exn @@ Script_int.to_int n

  let tez n = Option.unopt_exn @@ Tez.of_mutez @@ Int64.of_int n

  let left a = L a

  let right b = R b
end
