/*
 * Copyright (c) 2011-2013 libbitcoin developers (see AUTHORS)
 *
 * This file is part of libbitcoin.
 *
 * libbitcoin is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License with
 * additional permissions to the one published by the Free Software
 * Foundation, either version 3 of the License, or (at your option)
 * any later version. For more information see LICENSE.
 *
 * This program 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */
/*
  Full node implementation. Expects the blockchain to be present in
  "./blockchain/" and initialized using ./initchain
*/
#include <future>
#include "parser.hpp"
#include <bitcoin/bitcoin.hpp>
using namespace bc;

using std::placeholders::_1;
using std::placeholders::_2;
using std::placeholders::_3;
using std::placeholders::_4;

void log_to_file(std::ofstream& file, log_level level,
		 const std::string& domain, const std::string& body)
{
   if (body.empty())
      return;
   file << level_repr(level);
   if (!domain.empty())
      file << " [" << domain << "]";
   file << ": " << body << std::endl;
}
void log_to_both(std::ostream& device, std::ofstream& file, log_level level,
		 const std::string& domain, const std::string& body)
{
   if (body.empty())
      return;
   std::ostringstream output;
   output << level_repr(level);
   if (!domain.empty())
      output << " [" << domain << "]";
   output << ": " << body;
   device << output.str() << std::endl;
   file << output.str() << std::endl;
}

void output_file(std::ofstream& file, log_level level,
		 const std::string& domain, const std::string& body)
{
   log_to_file(file, level, domain, body);
}
void output_both(std::ofstream& file, log_level level,
		 const std::string& domain, const std::string& body)
{
   log_to_both(std::cout, file, level, domain, body);
}

void error_file(std::ofstream& file, log_level level,
		const std::string& domain, const std::string& body)
{
   log_to_file(file, level, domain, body);
}
void error_both(std::ofstream& file, log_level level,
		const std::string& domain, const std::string& body)
{
   log_to_both(std::cerr, file, level, domain, body);
}

class fullnode
{
public:
   fullnode();
   void start();
   // Should only be called from the main thread.
   // It's an error to join() a thread from inside it.
   void stop();

   blockchain& chain();
   transaction_indexer& indexer();

private:
   void handle_start(const std::error_code& ec);

   // New connection has been started.
   // Subscribe to new transaction messages from the network.
   void connection_started(const std::error_code& ec, channel_ptr node);
   // New transaction message from the network.
   // Attempt to validate it by storing it in the transaction pool.
   void recv_tx(const std::error_code& ec,
		const transaction_type& tx, channel_ptr node);
   // Result of store operation in transaction pool.
   void new_unconfirm_valid_tx(
      const std::error_code& ec, const index_list& unconfirmed,
      const transaction_type& tx);

   void handle_reorganize(
      const std::error_code& ec,   // Status of operation
      size_t fork_point,           // Index where blockchain forks
      const blockchain::block_list& added,     // New blocks added to blockchain
      const blockchain::block_list& removed    // Blocks removed (empty if none)
      );

   // Threadpools
   threadpool net_pool_, disk_pool_, mem_pool_;
   // Services
   hosts hosts_;
   handshake handshake_;
   network network_;
   protocol protocol_;
   leveldb_blockchain chain_;
   poller poller_;
   transaction_pool txpool_;
   //transaction_indexer txidx_;
   // Mac OSX needs the bc:: namespace qualifier to compile.
   // Other systems should be OK.
   bc::session session_;
   parser *parse;
};

fullnode::fullnode()
   // Threadpools and the number of threads they spawn.
   // 6 threads spawned in total.
   : net_pool_(1), disk_pool_(4), mem_pool_(1),
     // Networking related services.
     hosts_(net_pool_), handshake_(net_pool_), network_(net_pool_),
     protocol_(net_pool_, hosts_, handshake_, network_),
     // Blockchain database service.
     chain_(disk_pool_),
     // Poll new blocks, and transaction memory pool.
     poller_(mem_pool_, chain_), txpool_(mem_pool_, chain_), //txidx_(mem_pool_),
     // Session manager service. Convenience wrapper.
     session_(net_pool_, {
	   handshake_, protocol_, chain_, poller_, txpool_})
{
}


void fullnode::handle_reorganize(
   const std::error_code& ec,   // Status of operation
   size_t fork_point,           // Index where blockchain forks
   const blockchain::block_list& added,     // New blocks added to blockchain
   const blockchain::block_list& removed    // Blocks removed (empty if none)
   ) {

   if (!ec) {
      for (size_t i = 0; i < added.size(); ++i)
      {
	 parse->update(*added[i]);
      }
   }
   else {
      log_error() << "Problem reorganizing" << ec.message();
   }
   auto handle = bind(&fullnode::handle_reorganize, this, _1, _2,
		      _3, _4);
   chain_.subscribe_reorganize(handle);
}
void fullnode::start()
{
   // Subscribe to new connections.
   protocol_.subscribe_channel(
      std::bind(&fullnode::connection_started, this, _1, _2));
   // Start blockchain. Must finish before any operations
   // are performed on the database (or they will fail).
   std::promise<std::error_code> ec_promise;
   auto blockchain_started =
      [&ec_promise](const std::error_code& ec)
      {
	 ec_promise.set_value(ec);
      };
   chain_.start("blockchain", blockchain_started);
   std::error_code ec = ec_promise.get_future().get();
   if (ec)
   {
      log_error() << "Problem starting blockchain: " << ec.message();
      return;
   }
   parse = new parser(&chain_, false);
   auto handle = bind(&fullnode::handle_reorganize, this, _1, _2,
		      _3, _4);
   chain_.subscribe_reorganize(handle);
   // Fire off app.
   auto handle_start =
      std::bind(&fullnode::handle_start, this, _1);
   session_.start(handle_start);
}

void fullnode::stop()
{
   std::promise<std::error_code> ec_promise;
   auto session_stopped =
      [&ec_promise](const std::error_code& ec)
      {
	 ec_promise.set_value(ec);
      };
   session_.stop(session_stopped);
   std::error_code ec = ec_promise.get_future().get();
   if (ec)
      log_error() << "Problem stopping session: " << ec.message();

   
   // Stop threadpools.
   net_pool_.stop();
   disk_pool_.stop();
   mem_pool_.stop();
   // Join threadpools. Wait for them to finish.
   net_pool_.join();
   disk_pool_.join();
   mem_pool_.join();

   // Safely close blockchain database.
   chain_.stop();

   parse->close();

}

blockchain& fullnode::chain()
{
   return chain_;
}

void fullnode::handle_start(const std::error_code& ec)
{
   if (ec)
      log_error() << "fullnode: " << ec.message();
}

void fullnode::connection_started(const std::error_code& ec, channel_ptr node)
{
   if (ec)
   {
      log_warning() << "Couldn't start connection: " << ec.message();
      return;
   }
   protocol_.subscribe_channel(
      std::bind(&fullnode::connection_started, this, _1, _2));
}

int main()
{
   std::ofstream outfile("debug.log"), errfile("error.log");
   log_debug().set_output_function(
      std::bind(output_file, std::ref(outfile), _1, _2, _3));
   log_info().set_output_function(
      std::bind(output_both, std::ref(outfile), _1, _2, _3));
   log_warning().set_output_function(
      std::bind(error_file, std::ref(errfile), _1, _2, _3));
   log_error().set_output_function(
      std::bind(error_both, std::ref(errfile), _1, _2, _3));
   log_fatal().set_output_function(
      std::bind(error_both, std::ref(errfile), _1, _2, _3));

   fullnode app;
   app.start();
   while (true)
   {
      std::string addr;
      std::getline(std::cin, addr);
      if (addr == "stop")
	 break;
   }
   app.stop();

   return 0;
}
