#include <graphene/peerplays_sidechain/ethereum/transaction.hpp>

#include <boost/algorithm/hex.hpp>
#include <boost/algorithm/string.hpp>
#include <boost/property_tree/json_parser.hpp>
#include <boost/property_tree/ptree.hpp>

#include <secp256k1_recovery.h>
#include <sha3/sha3.h>

#include <fc/crypto/elliptic.hpp>
#include <fc/crypto/hex.hpp>

#include <graphene/peerplays_sidechain/ethereum/decoders.hpp>
#include <graphene/peerplays_sidechain/ethereum/encoders.hpp>
#include <graphene/peerplays_sidechain/ethereum/utils.hpp>

namespace graphene { namespace peerplays_sidechain { namespace ethereum {

const secp256k1_context *eth_context() {
   static secp256k1_context *ctx = secp256k1_context_create(SECP256K1_CONTEXT_VERIFY | SECP256K1_CONTEXT_SIGN);
   return ctx;
}

bytes keccak_hash(const std::string &data) {
   bytes hash;
   hash.resize(32);
   const auto transaction_string = boost::algorithm::unhex(remove_0x(data));
   keccak_256((const unsigned char *)transaction_string.data(), transaction_string.size(), (unsigned char *)hash.data());

   return hash;
}

signature sign_hash(const bytes &hash, const std::string &chain_id, const std::string &private_key) {
   const bytes priv_key = parse_hex(private_key);

   int recid = 0;
   secp256k1_ecdsa_recoverable_signature sig;
   FC_ASSERT(secp256k1_ecdsa_sign_recoverable(eth_context(), &sig, (const unsigned char *)hash.data(), (const unsigned char *)priv_key.data(), NULL, NULL));
   fc::ecc::compact_signature result;
   FC_ASSERT(secp256k1_ecdsa_recoverable_signature_serialize_compact(eth_context(), (unsigned char *)result.begin() + 1, &recid, &sig));

   unsigned int v = recid + from_hex<unsigned int>(chain_id) * 2 + 35;

   bytes r;
   for (int i = 1; i < 33; i++)
      r.emplace_back((char)result.at(i));

   bytes s;
   for (int i = 33; i < 65; i++)
      s.emplace_back((char)result.at(i));

   signature eth_sig;
   eth_sig.v = to_hex(v);
   eth_sig.r = fc::to_hex((char *)&r[0], r.size());
   eth_sig.s = fc::to_hex((char *)&s[0], s.size());

   return eth_sig;
}

//! base_transaction

base_transaction::base_transaction(const std::string &raw_tx) {
}

//! transaction

transaction::transaction(const std::string &raw_tx) :
      base_transaction{raw_tx} {
   deserialize(raw_tx);
}

const transaction &transaction::sign(const std::string &private_key) const {
   return *this;
}

std::string transaction::serialize() const {
   boost::property_tree::ptree pt;
   pt.put("from", from);
   pt.put("to", to);
   pt.put("data", data);

   std::stringstream ss;
   boost::property_tree::json_parser::write_json(ss, pt);
   return ss.str();
}

void transaction::deserialize(const std::string &raw_tx) {
   std::stringstream ss_tx(raw_tx);
   boost::property_tree::ptree tx_json;
   boost::property_tree::read_json(ss_tx, tx_json);

   if (tx_json.count("from"))
      from = tx_json.get<std::string>("from");
   if (tx_json.count("to"))
      to = tx_json.get<std::string>("to");
   if (tx_json.count("data"))
      data = tx_json.get<std::string>("data");
}

//! raw_transaction

raw_transaction::raw_transaction(const std::string &raw_tx) :
      base_transaction{raw_tx} {
   deserialize(raw_tx);
}

bytes raw_transaction::hash() const {
   return keccak_hash(serialize());
}

signed_transaction raw_transaction::sign(const std::string &private_key) const {
   //! Prepare signed transaction
   signed_transaction tr;
   tr.nonce = nonce;
   tr.gas_price = gas_price;
   tr.gas_limit = gas_limit;
   tr.to = to;
   tr.value = value;
   tr.data = data;

   const auto sig = sign_hash(hash(), chain_id, private_key);
   tr.v = sig.v;
   tr.r = sig.r;
   tr.s = sig.s;

   return tr;
}

std::string raw_transaction::serialize() const {
   const std::string serialized = rlp_encoder::encode(remove_0x(nonce)) +
                                  rlp_encoder::encode(remove_0x(gas_price)) +
                                  rlp_encoder::encode(remove_0x(gas_limit)) +
                                  rlp_encoder::encode(remove_0x(to)) +
                                  rlp_encoder::encode(remove_0x(value)) +
                                  rlp_encoder::encode(remove_0x(data)) +
                                  rlp_encoder::encode(remove_0x(chain_id)) +
                                  rlp_encoder::encode("") +
                                  rlp_encoder::encode("");

   return add_0x(bytes2hex(rlp_encoder::encode_length(serialized.size(), 192) + serialized));
}

void raw_transaction::deserialize(const std::string &raw_tx) {
   const auto rlp_array = rlp_decoder::decode(remove_0x(raw_tx));
   FC_ASSERT(rlp_array.size() >= 7, "Wrong rlp format");

   nonce = !rlp_array.at(0).empty() ? add_0x(rlp_array.at(0)) : add_0x("0");
   boost::algorithm::to_lower(nonce);
   gas_price = add_0x(rlp_array.at(1));
   boost::algorithm::to_lower(gas_price);
   gas_limit = add_0x(rlp_array.at(2));
   boost::algorithm::to_lower(gas_limit);
   to = add_0x(rlp_array.at(3));
   boost::algorithm::to_lower(to);
   value = !rlp_array.at(4).empty() ? add_0x(rlp_array.at(4)) : add_0x("0");
   boost::algorithm::to_lower(value);
   data = !rlp_array.at(5).empty() ? add_0x(rlp_array.at(5)) : "";
   boost::algorithm::to_lower(data);
   chain_id = add_0x(rlp_array.at(6));
   boost::algorithm::to_lower(chain_id);
}

//! signed_transaction

signed_transaction::signed_transaction(const std::string &raw_tx) :
      base_transaction{raw_tx} {
   deserialize(raw_tx);
}

std::string signed_transaction::recover(const std::string &chain_id) const {
   fc::ecc::compact_signature input64;
   fc::from_hex(r, (char *)&input64.at(1), 32);
   const int recid = from_hex<unsigned int>(v) - from_hex<unsigned int>(chain_id) * 2 - 35;
   fc::from_hex(std::to_string(recid), (char *)&input64.at(0), 1);
   fc::from_hex(s, (char *)&input64.at(33), 32);

   secp256k1_ecdsa_recoverable_signature sig;
   FC_ASSERT(secp256k1_ecdsa_recoverable_signature_parse_compact(eth_context(), &sig, (const unsigned char *)&input64.data[1], recid));

   raw_transaction tr;
   tr.nonce = nonce;
   tr.gas_price = gas_price;
   tr.gas_limit = gas_limit;
   tr.to = to;
   tr.value = value;
   tr.data = data;
   tr.chain_id = chain_id;

   secp256k1_pubkey rawPubkey;
   FC_ASSERT(secp256k1_ecdsa_recover(eth_context(), &rawPubkey, &sig, (const unsigned char *)tr.hash().data()));

   std::array<uint8_t, 65> pubkey;
   size_t biglen = 65;
   FC_ASSERT(secp256k1_ec_pubkey_serialize(eth_context(), pubkey.data(), &biglen, &rawPubkey, SECP256K1_EC_UNCOMPRESSED));

   const std::string out = std::string(pubkey.begin(), pubkey.end()).substr(1);
   bytes hash;
   hash.resize(32);
   keccak_256((const unsigned char *)out.data(), out.size(), (unsigned char *)hash.data());

   return add_0x(fc::to_hex((char *)&hash[0], hash.size()).substr(24));
}

std::string signed_transaction::serialize() const {
   const std::string serialized = rlp_encoder::encode(remove_0x(nonce)) +
                                  rlp_encoder::encode(remove_0x(gas_price)) +
                                  rlp_encoder::encode(remove_0x(gas_limit)) +
                                  rlp_encoder::encode(remove_0x(to)) +
                                  rlp_encoder::encode(remove_0x(value)) +
                                  rlp_encoder::encode(remove_0x(data)) +
                                  rlp_encoder::encode(remove_0x(v)) +
                                  rlp_encoder::encode(remove_leading_00(remove_0x(r))) +
                                  rlp_encoder::encode(remove_leading_00(remove_0x(s)));

   return add_0x(bytes2hex(rlp_encoder::encode_length(serialized.size(), 192) + serialized));
}

void signed_transaction::deserialize(const std::string &raw_tx) {
   const auto rlp_array = rlp_decoder::decode(remove_0x(raw_tx));
   FC_ASSERT(rlp_array.size() >= 9, "Wrong rlp format");

   nonce = !rlp_array.at(0).empty() ? add_0x(rlp_array.at(0)) : add_0x("0");
   boost::algorithm::to_lower(nonce);
   gas_price = add_0x(rlp_array.at(1));
   boost::algorithm::to_lower(gas_price);
   gas_limit = add_0x(rlp_array.at(2));
   boost::algorithm::to_lower(gas_limit);
   to = add_0x(rlp_array.at(3));
   boost::algorithm::to_lower(to);
   value = !rlp_array.at(4).empty() ? add_0x(rlp_array.at(4)) : add_0x("0");
   boost::algorithm::to_lower(value);
   data = !rlp_array.at(5).empty() ? add_0x(rlp_array.at(5)) : "";
   boost::algorithm::to_lower(data);
   v = add_0x(rlp_array.at(6));
   boost::algorithm::to_lower(v);
   r = add_0x(add_leading_00(rlp_array.at(7)));
   boost::algorithm::to_lower(r);
   s = add_0x(add_leading_00(rlp_array.at(8)));
   boost::algorithm::to_lower(s);
}

}}} // namespace graphene::peerplays_sidechain::ethereum
