module Signature = Tezos_base.TzPervasives.Signature
open Proto_alpha_utils.Memory_proto_alpha
module Data_encoding = Alpha_environment.Data_encoding
module MBytes = Alpha_environment.MBytes
module Error_monad = Proto_alpha_utils.Error_monad
open Error_monad

module Context_init = struct

  type account = {
      pkh : Signature.Public_key_hash.t ;
      pk :  Signature.Public_key.t ;
      sk :  Signature.Secret_key.t ;
    }

  let generate_accounts n : (account * Tez_repr.t) list =
    let amount = Tez_repr.of_mutez_exn 4_000_000_000_000L in
    List.map (fun _ ->
        let (pkh, pk, sk) = Signature.generate_key () in
        let account = { pkh ; pk ; sk } in
        account, amount)
      (Simple_utils.List.range n)

  let make_shell
        ~level ~predecessor ~timestamp ~fitness ~operations_hash =
    Tezos_base.Block_header.{
        level ;
        predecessor ;
        timestamp ;
        fitness ;
        operations_hash ;
        (* We don't care of the following values, only the shell validates them. *)
        proto_level = 0 ;
        validation_passes = 0 ;
        context = Alpha_environment.Context_hash.zero ;
    }

  let default_proof_of_work_nonce =
    MBytes.create Alpha_context.Constants.proof_of_work_nonce_size

  let protocol_param_key = [ "protocol_parameters" ]

  let check_constants_consistency constants =
    let open Constants_repr in
    let open Error_monad in
    let { blocks_per_cycle ; blocks_per_commitment ;
          blocks_per_roll_snapshot ; _ } = constants in
    Error_monad.unless (blocks_per_commitment <= blocks_per_cycle)
      (fun () -> failwith "Inconsistent constants : blocks per commitment must be \
                           less than blocks per cycle") >>=? fun () ->
    Error_monad.unless (blocks_per_cycle >= blocks_per_roll_snapshot)
      (fun () -> failwith "Inconsistent constants : blocks per cycle \
                           must be superior than blocks per roll snapshot") >>=?
      return


  let initial_context
        constants
        header
        commitments
        initial_accounts
        security_deposit_ramp_up_cycles
        no_reward_cycles
    =
    let open Tezos_base.TzPervasives.Error_monad in
    let bootstrap_accounts =
      List.map (fun ({ pk ; pkh ; _ }, amount) ->
          let open! Parameters_repr in
          { public_key_hash = pkh ; public_key = Some pk ; amount }
        ) initial_accounts
    in
    let json =
      Data_encoding.Json.construct
        Parameters_repr.encoding
        Parameters_repr.{
          bootstrap_accounts ;
          bootstrap_contracts = [] ;
          commitments ;
          constants ;
          security_deposit_ramp_up_cycles ;
          no_reward_cycles ;
      }
    in
    let proto_params =
      Data_encoding.Binary.to_bytes_exn Data_encoding.json json
    in
    Tezos_protocol_environment_memory.Context.(
      set empty ["version"] (MBytes.of_string "genesis")
    ) >>= fun ctxt ->
    Tezos_protocol_environment_memory.Context.(
      set ctxt protocol_param_key proto_params
    ) >>= fun ctxt ->
    Main.init ctxt header
    >|= Alpha_environment.wrap_error >>=? fun { context; _ } ->
    return context

  let genesis
        ?(preserved_cycles = Constants_repr.default.preserved_cycles)
        ?(blocks_per_cycle = Constants_repr.default.blocks_per_cycle)
        ?(blocks_per_commitment = Constants_repr.default.blocks_per_commitment)
        ?(blocks_per_roll_snapshot = Constants_repr.default.blocks_per_roll_snapshot)
        ?(blocks_per_voting_period = Constants_repr.default.blocks_per_voting_period)
        ?(time_between_blocks = Constants_repr.default.time_between_blocks)
        ?(endorsers_per_block = Constants_repr.default.endorsers_per_block)
        ?(hard_gas_limit_per_operation = Constants_repr.default.hard_gas_limit_per_operation)
        ?(hard_gas_limit_per_block = Constants_repr.default.hard_gas_limit_per_block)
        ?(proof_of_work_threshold = Int64.(neg one))
        ?(tokens_per_roll = Constants_repr.default.tokens_per_roll)
        ?(michelson_maximum_type_size = Constants_repr.default.michelson_maximum_type_size)
        ?(seed_nonce_revelation_tip = Constants_repr.default.seed_nonce_revelation_tip)
        ?(origination_size = Constants_repr.default.origination_size)
        ?(block_security_deposit = Constants_repr.default.block_security_deposit)
        ?(endorsement_security_deposit = Constants_repr.default.endorsement_security_deposit)
        ?(block_reward = Constants_repr.default.block_reward)
        ?(endorsement_reward = Constants_repr.default.endorsement_reward)
        ?(cost_per_byte = Constants_repr.default.cost_per_byte)
        ?(hard_storage_limit_per_operation = Constants_repr.default.hard_storage_limit_per_operation)
        ?(commitments = [])
        ?(security_deposit_ramp_up_cycles = None)
        ?(no_reward_cycles = None)
        (initial_accounts : (account * Tez_repr.t) list)
    =
    if initial_accounts = [] then
      Pervasives.failwith "Must have one account with a roll to bake";

    (* Check there is at least one roll *)
    let open Tezos_base.TzPervasives.Error_monad in
    begin try
        let (>>?=) x y = match x with
          | Ok(a) -> y a
          | Error(b) -> fail @@ List.hd b in
        fold_left_s (fun acc (_, amount) ->
            Alpha_environment.wrap_error @@
              Tez_repr.(+?) acc amount >>?= fun acc ->
                                            if acc >= tokens_per_roll then
                                              raise Exit
                                            else return acc
          ) Tez_repr.zero initial_accounts >>=? fun _ ->
      failwith "Insufficient tokens in initial accounts to create one roll"
    with Exit -> return ()
    end >>=? fun () ->

    let constants : Constants_repr.parametric = {
        preserved_cycles ;
        blocks_per_cycle ;
        blocks_per_commitment ;
        blocks_per_roll_snapshot ;
        blocks_per_voting_period ;
        time_between_blocks ;
        endorsers_per_block ;
        hard_gas_limit_per_operation ;
        hard_gas_limit_per_block ;
        proof_of_work_threshold ;
        tokens_per_roll ;
        michelson_maximum_type_size ;
        seed_nonce_revelation_tip ;
        origination_size ;
        block_security_deposit ;
        endorsement_security_deposit ;
        block_reward ;
        endorsement_reward ;
        cost_per_byte ;
        hard_storage_limit_per_operation ;
      } in
    check_constants_consistency constants >>=? fun () ->

    let hash =
      Alpha_environment.Block_hash.of_b58check_exn "BLockGenesisGenesisGenesisGenesisGenesisCCCCCeZiLHU"
    in
    let shell = make_shell
                  ~level:0l
                  ~predecessor:hash
                  ~timestamp:Tezos_utils.Time.epoch
                  ~fitness: (Fitness_repr.from_int64 0L)
                  ~operations_hash: Alpha_environment.Operation_list_list_hash.zero in
    initial_context
      constants
      shell
      commitments
      initial_accounts
      security_deposit_ramp_up_cycles
      no_reward_cycles
    >>=? fun context ->
    return (context, shell, hash)

  let init
        ?(slow=false)
        ?preserved_cycles
        ?endorsers_per_block
        ?commitments
        n =
    let open Error_monad in
    let accounts = generate_accounts n in
    let contracts = List.map (fun (a, _) ->
                        Alpha_context.Contract.implicit_contract (a.pkh)) accounts in
    begin
      if slow then
        genesis
          ?preserved_cycles
          ?endorsers_per_block
          ?commitments
          accounts
      else
        genesis
          ?preserved_cycles
          ~blocks_per_cycle:32l
          ~blocks_per_commitment:4l
          ~blocks_per_roll_snapshot:8l
          ~blocks_per_voting_period:(Int32.mul 32l 8l)
          ?endorsers_per_block
          ?commitments
          accounts
    end >>=? fun ctxt ->
    return (ctxt, accounts, contracts)

  let contents
        ?(proof_of_work_nonce = default_proof_of_work_nonce)
        ?(priority = 0) ?seed_nonce_hash () =
    Alpha_context.Block_header.({
      priority ;
      proof_of_work_nonce ;
      seed_nonce_hash ;
    })


  let begin_construction ?(priority=0) ~timestamp ~(header:Alpha_context.Block_header.shell_header) ~hash ctxt =
    let contents = contents ~priority () in
    let protocol_data =
      let open! Alpha_context.Block_header in {
        contents ;
        signature = Signature.zero ;
      } in
    let header = {
        Alpha_context.Block_header.shell = {
          predecessor = hash ;
          proto_level = header.proto_level ;
          validation_passes = header.validation_passes ;
          fitness = header.fitness ;
          timestamp ;
          level = header.level ;
          context = Alpha_environment.Context_hash.zero ;
          operations_hash = Alpha_environment.Operation_list_list_hash.zero ;
        } ;
        protocol_data = {
            contents ;
            signature = Signature.zero ;
          } ;
      } in
    Main.begin_construction
      ~chain_id: Alpha_environment.Chain_id.zero
      ~predecessor_context: ctxt
      ~predecessor_timestamp: header.shell.timestamp
      ~predecessor_fitness: header.shell.fitness
      ~predecessor_level: header.shell.level
      ~predecessor:hash
      ~timestamp
      ~protocol_data
      () >>= fun x -> Lwt.return @@ Alpha_environment.wrap_error x >>=? fun state ->
    return state.ctxt

  let main n =
    init n >>=? fun ((ctxt, header, hash), accounts, contracts) ->
    let timestamp = Tezos_base.Time.now () in
    begin_construction ~timestamp ~header ~hash ctxt >>=? fun ctxt ->
    return (ctxt, accounts, contracts)

end

type identity = {
  public_key_hash : Signature.public_key_hash;
  public_key : Signature.public_key;
  secret_key : Signature.secret_key;
  implicit_contract : Alpha_context.Contract.t;
}

type environment = {
  tezos_context : Alpha_context.t ;
  identities : identity list ;
}

let init_environment () =
  Context_init.main 10 >>=? fun (tezos_context, accounts, contracts) ->
  let accounts = List.map fst accounts in
  let tezos_context = Alpha_context.Gas.set_limit tezos_context @@ Z.of_int 350000 in
  let identities =
    List.map (fun ((a:Context_init.account), c) -> {
          public_key = a.pk ;
          public_key_hash = a.pkh ;
          secret_key = a.sk ;
          implicit_contract = c ;
        }) @@
    List.combine accounts contracts in
  return {tezos_context ; identities}

let contextualize ~msg ?environment f =
  let lwt =
    let environment = match environment with
      | None -> init_environment ()
      | Some x -> return x in
    environment >>=? f
  in
  force_ok ~msg @@ Lwt_main.run lwt
