(**************************************************************************)
(*                                                                        *)
(*    Copyright (c) 2014 - 2018.                                          *)
(*    Dynamic Ledger Solutions, Inc. <contact@tezos.com>                  *)
(*                                                                        *)
(*    All rights reserved. No warranty, explicit or implicit, provided.   *)
(*                                                                        *)
(**************************************************************************)

include Logging.Make(struct let name = "node.validator" end)

type t = {

  state: State.t ;
  db: Distributed_db.t ;
  block_validator: Block_validator.t ;
  net_validator_limits: Net_validator.limits ;
  peer_validator_limits: Peer_validator.limits ;
  block_validator_limits: Block_validator.limits ;
  prevalidator_limits: Prevalidator.limits ;

  valid_block_input: State.Block.t Lwt_watcher.input ;
  active_nets: Net_validator.t Lwt.t Net_id.Table.t ;

}

let create state db
    peer_validator_limits
    block_validator_limits
    prevalidator_limits
    net_validator_limits =
  Block_validator.create block_validator_limits db >>= fun block_validator ->
  let valid_block_input = Lwt_watcher.create_input () in
  Lwt.return
    { state ; db ; block_validator ;
      block_validator_limits ; prevalidator_limits ;
      peer_validator_limits ; net_validator_limits ;
      valid_block_input ;
      active_nets = Net_id.Table.create 7 }

let activate v ?max_child_ttl net_state =
  let net_id = State.Net.id net_state in
  lwt_log_notice "activate network %a" Net_id.pp net_id >>= fun () ->
  try Net_id.Table.find v.active_nets net_id
  with Not_found ->
    let nv =
      Net_validator.create
        ?max_child_ttl
        v.peer_validator_limits v.prevalidator_limits
        v.block_validator v.valid_block_input v.db net_state
        v.net_validator_limits in
    Net_id.Table.add v.active_nets net_id nv ;
    nv

let get_exn { active_nets } net_id =
  Net_id.Table.find active_nets net_id

type error +=
  | Inactive_network of Net_id.t

let () =
  register_error_kind `Branch
    ~id: "node.validator.inactive_network"
    ~title: "Inactive network"
    ~description: "Attempted validation of a block from an inactive network."
    ~pp: (fun ppf net ->
        Format.fprintf ppf
          "Tried to validate a block from network %a, \
           that is not currently considered active."
          Net_id.pp net)
    Data_encoding.(obj1 (req "inactive_network" Net_id.encoding))
    (function Inactive_network net -> Some net | _ -> None)
    (fun net -> Inactive_network net)

let get v net_id =
  try get_exn v net_id >>= fun nv -> return nv
  with Not_found -> fail (Inactive_network net_id)

let validate_block v ?(force = false) ?net_id bytes operations =
  let hash = Block_hash.hash_bytes [bytes] in
  match Block_header.of_bytes bytes with
  | None -> failwith "Cannot parse block header."
  | Some block ->
      begin
        match net_id with
        | None -> begin
            Distributed_db.read_block_header
              v.db block.shell.predecessor >>= function
            | None ->
                failwith "Unknown predecessor (%a), cannot inject the block."
                  Block_hash.pp_short block.shell.predecessor
            | Some (net_id, _bh) -> get v net_id
          end
        | Some net_id ->
            get v net_id >>=? fun nv ->
            if force then
              return nv
            else
              Distributed_db.Block_header.known
                (Net_validator.net_db nv)
                block.shell.predecessor >>= function
              | true ->
                  return nv
              | false ->
                  failwith "Unknown predecessor (%a), cannot inject the block."
                    Block_hash.pp_short block.shell.predecessor
      end >>=? fun nv ->
      let validation =
        Net_validator.validate_block nv ~force hash block operations in
      return (hash, validation)

let shutdown { active_nets ; block_validator } =
  let jobs =
    Block_validator.shutdown block_validator ::
    Net_id.Table.fold
      (fun _ nv acc -> (nv >>= Net_validator.shutdown) :: acc)
      active_nets [] in
  Lwt.join jobs >>= fun () ->
  Lwt.return_unit

let watcher { valid_block_input } =
  Lwt_watcher.create_stream valid_block_input

let inject_operation v ?net_id op =
  begin
    match net_id with
    | None -> begin
        Distributed_db.read_block_header
          v.db op.Operation.shell.branch >>= function
        | None ->
            failwith "Unknown branch (%a), cannot inject the operation."
              Block_hash.pp_short op.shell.branch
        | Some (net_id, _bh) -> get v net_id
      end
    | Some net_id ->
        get v net_id >>=? fun nv ->
        Distributed_db.Block_header.known
          (Net_validator.net_db nv)
          op.shell.branch >>= function
        | true ->
            return nv
        | false ->
            failwith "Unknown branch (%a), cannot inject the operation."
              Block_hash.pp_short op.shell.branch
  end >>=? fun nv ->
  let pv = Net_validator.prevalidator nv in
  Prevalidator.inject_operation pv op
