#include <hive/plugins/witness/block_producer.hpp>

#include <hive/protocol/base.hpp>
#include <hive/protocol/config.hpp>
#include <hive/protocol/version.hpp>

#include <hive/chain/database.hpp>
#include <hive/chain/database_exceptions.hpp>
#include <hive/chain/db_with.hpp>
#include <hive/chain/pending_required_action_object.hpp>
#include <hive/chain/pending_optional_action_object.hpp>
#include <hive/chain/witness_objects.hpp>

#include <fc/macros.hpp>

namespace hive { namespace plugins { namespace witness {

void block_producer::generate_block( chain::generate_block_flow_control* generate_block_ctrl )
{
  hive::chain::detail::with_skip_flags( _db, generate_block_ctrl->get_skip_flags(), [&]()
  {
    try
    {
      _generate_block( generate_block_ctrl, generate_block_ctrl->get_block_timestamp(), generate_block_ctrl->get_witness_owner(),
                       generate_block_ctrl->get_block_signing_private_key() );
    }
    FC_CAPTURE_AND_RETHROW( ( generate_block_ctrl->get_witness_owner() ) )
  } );
}

void block_producer::_generate_block( chain::generate_block_flow_control* generate_block_ctrl,
                                      fc::time_point_sec when, const chain::account_name_type& witness_owner,
                                      const fc::ecc::private_key& block_signing_private_key)
{
  uint32_t skip = _db.get_node_properties().skip_flags;
  uint32_t slot_num = _db.get_slot_at_time( when );
  FC_ASSERT( slot_num > 0 );
  string scheduled_witness = _db.get_scheduled_witness( slot_num );
  FC_ASSERT( scheduled_witness == witness_owner );

  const auto& witness_obj = _db.get_witness( witness_owner );

  if( !(skip & chain::database::skip_witness_signature) )
    FC_ASSERT( witness_obj.signing_key == block_signing_private_key.get_public_key() );

  chain::signed_block_header pending_block_header;

  pending_block_header.previous = _db.head_block_id();
  pending_block_header.timestamp = when;
  pending_block_header.witness = witness_owner;

  adjust_hardfork_version_vote( _db.get_witness( witness_owner ), pending_block_header );

  std::vector<std::shared_ptr<hive::chain::full_transaction_type>> full_transactions;
  apply_pending_transactions(witness_owner, when, pending_block_header, full_transactions);

  // We have temporarily broken the invariant that
  // _pending_tx_session is the result of applying _pending_tx, as
  // _pending_tx now consists of the set of postponed transactions.
  // However, the push_block() call below will re-create the
  // _pending_tx_session.

  const fc::ecc::private_key* signer = (skip & chain::database::skip_witness_signature) ? nullptr : &block_signing_private_key;

  std::shared_ptr<hive::chain::full_block_type> full_pending_block = 
    hive::chain::full_block_type::create_from_block_header_and_transactions(pending_block_header, full_transactions, signer);

  // TODO:  Move this to _push_block() so session is restored.
  if( !(skip & chain::database::skip_block_size_check) )
    FC_ASSERT(full_pending_block->get_uncompressed_block_size() <= HIVE_MAX_BLOCK_SIZE );
  generate_block_ctrl->store_produced_block( full_pending_block );

  try
  {
    _db.push_block( *generate_block_ctrl, skip );
  }
  catch (const fc::exception& e)
  {
    elog("NOTIFYALERT! Failed to apply newly produced block ${block_num} (${block_id}) with exception ${e}", 
         ("block_num", full_pending_block->get_block_num())("block_id", full_pending_block->get_block_id())(e));
    throw;
  }
}

void block_producer::adjust_hardfork_version_vote(const chain::witness_object& witness, chain::signed_block_header& pending_block_header)
{
  using namespace hive::protocol;

  if( witness.running_version != HIVE_BLOCKCHAIN_VERSION )
    pending_block_header.extensions.insert( block_header_extensions( HIVE_BLOCKCHAIN_VERSION ) );

  const auto& hfp = _db.get_hardfork_property_object();
  const auto& hf_versions = _db.get_hardfork_versions();

  if( hfp.current_hardfork_version < HIVE_BLOCKCHAIN_VERSION // Binary is newer hardfork than has been applied
    && ( witness.hardfork_version_vote != hf_versions.versions[ hfp.last_hardfork + 1 ] || witness.hardfork_time_vote != hf_versions.times[ hfp.last_hardfork + 1 ] ) ) // Witness vote does not match binary configuration
  {
    // Make vote match binary configuration
    pending_block_header.extensions.insert( block_header_extensions( hardfork_version_vote( hf_versions.versions[ hfp.last_hardfork + 1 ], hf_versions.times[ hfp.last_hardfork + 1 ] ) ) );
  }
  else if( hfp.current_hardfork_version == HIVE_BLOCKCHAIN_VERSION // Binary does not know of a new hardfork
        && witness.hardfork_version_vote > HIVE_BLOCKCHAIN_VERSION ) // Voting for hardfork in the future, that we do not know of...
  {
    // Make vote match binary configuration. This is vote to not apply the new hardfork.
    pending_block_header.extensions.insert( block_header_extensions( hardfork_version_vote( hf_versions.versions[ hfp.last_hardfork ], hf_versions.times[ hfp.last_hardfork ] ) ) );
  }
}

void block_producer::apply_pending_transactions(const chain::account_name_type& witness_owner,
                                                fc::time_point_sec when,
                                                chain::signed_block_header& pending_block_header, 
                                                std::vector<std::shared_ptr<hive::chain::full_transaction_type>>& full_transactions)
{
  // The 4 is for the max size of the transaction vector length
  //ABW: size of vector can take between 1 and 5 packed bytes, therefore +4 covers potential max (size of empty is already
  //included in uncorrected pack_size; in practice total_block_size will overshoot actual pack_size by 4 or 3 bytes, maybe
  //2 bytes if we ever allow blocks big enough to accomodate over 16k transactions (15 or more bits needed for size)
  size_t total_block_size = fc::raw::pack_size(pending_block_header) + 4;
  const auto& gpo = _db.get_dynamic_global_properties();
  uint64_t maximum_block_size = gpo.maximum_block_size; //HIVE_MAX_BLOCK_SIZE;
  uint64_t maximum_transaction_partition_size = maximum_block_size -  ( maximum_block_size * gpo.required_actions_partition_percent ) / HIVE_100_PERCENT;

  //
  // The following code throws away existing pending_tx_session and
  // rebuilds it by re-applying pending transactions.
  //
  // This rebuild is necessary because pending transactions' validity
  // and semantics may have changed since they were received, because
  // time-based semantics are evaluated based on the current block
  // time.  These changes can only be reflected in the database when
  // the value of the "when" variable is known, which means we need to
  // re-apply pending transactions in this method.
  //
  _db.pending_transaction_session().reset();
  _db.pending_transaction_session() = _db.start_undo_session();

  FC_TODO( "Safe to remove after HF20 occurs because no more pre HF20 blocks will be generated" );
  if( _db.has_hardfork( HIVE_HARDFORK_0_20 ) )
  {
    /// modify current witness so transaction evaluators can know who included the transaction
    _db.modify(_db.get_dynamic_global_properties(), [&]( chain::dynamic_global_property_object& dgp )
    {
      dgp.current_witness = witness_owner;
    } );
  }

  BOOST_SCOPE_EXIT( &_db ) { _db.clear_tx_status(); } BOOST_SCOPE_EXIT_END
  // the flag also covers time of processing of required and optional actions
  _db.set_tx_status( chain::database::TX_STATUS_GEN_BLOCK );

  uint32_t postponed_tx_count = 0;
  uint32_t failed_tx_count = 0;
  // pop pending state (reset to head block state)
  for( const std::shared_ptr<hive::chain::full_transaction_type>& full_transaction : _db._pending_tx )
  {
    const hive::protocol::signed_transaction& tx = full_transaction->get_transaction();
    // Only include transactions that have not expired yet for currently generating block,
    // this should clear problem transactions and allow block production to continue

    if( postponed_tx_count > HIVE_BLOCK_GENERATION_POSTPONED_TX_LIMIT )
      break;

    if( tx.expiration < when )
    {
      ++failed_tx_count;
      continue;
    }

    uint64_t new_total_size = total_block_size + full_transaction->get_transaction_size();

    // postpone transaction if it would make block too big
    if( new_total_size >= maximum_transaction_partition_size )
    {
      ++postponed_tx_count;
      continue;
    }

    try
    {
      auto temp_session = _db.start_undo_session();
      _db.apply_transaction(full_transaction, _db.get_node_properties().skip_flags);
      temp_session.squash();

      total_block_size = new_total_size;
      full_transactions.push_back(full_transaction);
    }
    catch ( const fc::exception& e )
    {
      ++failed_tx_count;
      // Do nothing, transaction will be re-applied after this block is reapplied (and possibly
      // after processing further blocks) until it expires or repeats the exception during that time
      //wlog( "Transaction was not processed while generating block due to ${e}", ("e", e) );
      //wlog( "The transaction was ${t}", ("t", tx) );
    }
  }
  if (postponed_tx_count > 0 || failed_tx_count > 0)
  {
    wlog("Postponed ${postponed_count} transactions during block production (${failed_tx_count} failed/expired)", 
         ("postponed_count", _db._pending_tx.size() - full_transactions.size())(failed_tx_count));
  }

  const auto& pending_required_action_idx = _db.get_index< chain::pending_required_action_index, chain::by_execution >();
  auto pending_required_itr = pending_required_action_idx.begin();
  chain::required_automated_actions required_actions;

  while( pending_required_itr != pending_required_action_idx.end() && pending_required_itr->execution_time <= when )
  {
    uint64_t new_total_size = total_block_size + fc::raw::pack_size( pending_required_itr->action );

    // required_actions_partizion_size is a lower bound of requirement.
    // If we have extra space to include actions we should use it.
    if( new_total_size > maximum_block_size )
      break;

    try
    {
      auto temp_session = _db.start_undo_session();
      _db.apply_required_action( pending_required_itr->action );
      temp_session.squash();
      total_block_size = new_total_size;
      required_actions.push_back( pending_required_itr->action );
      ++pending_required_itr;
    }
    catch( fc::exception& e )
    {
      FC_RETHROW_EXCEPTION( e, warn, "A required automatic action was rejected. ${a} ${e}", ("a", pending_required_itr->action)("e", e.to_detail_string()) );
    }
  }

FC_TODO( "Remove ifdef when required actions are added" )
#ifdef IS_TEST_NET
  if( required_actions.size() )
  {
    pending_block_header.extensions.insert( required_actions );
  }
#endif

  const auto& pending_optional_action_idx = _db.get_index< chain::pending_optional_action_index, chain::by_execution >();
  auto pending_optional_itr = pending_optional_action_idx.begin();
  chain::optional_automated_actions optional_actions;

  while( pending_optional_itr != pending_optional_action_idx.end() && pending_optional_itr->execution_time <= when )
  {
    uint64_t new_total_size = total_block_size + fc::raw::pack_size( pending_optional_itr->action );

    if( new_total_size > maximum_block_size )
      break;

    try
    {
      auto temp_session = _db.start_undo_session();
      _db.apply_optional_action( pending_optional_itr->action );
      temp_session.squash();
      total_block_size = new_total_size;
      optional_actions.push_back( pending_optional_itr->action );
    }
    catch( fc::exception& ) {}
    ++pending_optional_itr;
  }

FC_TODO( "Remove ifdef when optional actions are added" )
#ifdef IS_TEST_NET
  if( optional_actions.size() )
  {
    pending_block_header.extensions.insert( optional_actions );
  }
#endif

  _db.pending_transaction_session().reset();
}

} } } // hive::plugins::witness
