// Copyright (c) 2017-2018, The Monero Project
//
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification, are
// permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of
//    conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
//    of conditions and the following disclaimer in the documentation and/or other
//    materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be
//    used to endorse or promote products derived from this software without specific
//    prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//

#include "device_trezor.hpp"

namespace hw {
namespace trezor {

#ifdef WITH_DEVICE_TREZOR

#undef MONERO_DEFAULT_LOG_CATEGORY
#define MONERO_DEFAULT_LOG_CATEGORY "device.trezor"

#define HW_TREZOR_NAME "Trezor"

    static device_trezor *trezor_device = nullptr;
    static device_trezor *ensure_trezor_device(){
      if (!trezor_device) {
        trezor_device = new device_trezor();
        trezor_device->set_name(HW_TREZOR_NAME);
      }
      return trezor_device;
    }

    void register_all(std::map<std::string, std::unique_ptr<device>> &registry) {
      registry.insert(std::make_pair(HW_TREZOR_NAME, std::unique_ptr<device>(ensure_trezor_device())));
    }

    void register_all() {
      hw::register_device(HW_TREZOR_NAME, ensure_trezor_device());
    }

    device_trezor::device_trezor() {

    }

    device_trezor::~device_trezor() {
      try {
        disconnect();
        release();
      } catch(std::exception const& e){
        MWARNING("Could not disconnect and release: " << e.what());
      }
    }

    /* ======================================================================= */
    /*                             WALLET & ADDRESS                            */
    /* ======================================================================= */

    bool device_trezor::get_public_address(cryptonote::account_public_address &pubkey) {
      try {
        auto res = get_address();

        cryptonote::address_parse_info info{};
        bool r = cryptonote::get_account_address_from_str(info, this->network_type, res->address());
        CHECK_AND_ASSERT_MES(r, false, "Could not parse returned address. Address parse failed: " + res->address());
        CHECK_AND_ASSERT_MES(!info.is_subaddress, false, "Trezor returned a sub address");

        pubkey = info.address;
        return true;

      } catch(std::exception const& e){
        MERROR("Get public address exception: " << e.what());
        return false;
      }
    }

    bool device_trezor::get_secret_keys(crypto::secret_key &viewkey , crypto::secret_key &spendkey) {
      try {
        MDEBUG("Loading view-only key from the Trezor. Please check the Trezor for a confirmation.");
        auto res = get_view_key();
        CHECK_AND_ASSERT_MES(res->watch_key().size() == 32, false, "Trezor returned invalid view key");

        spendkey = crypto::null_skey; // not given
        memcpy(viewkey.data, res->watch_key().data(), 32);

        return true;

      } catch(std::exception const& e){
        MERROR("Get secret keys exception: " << e.what());
        return false;
      }
    }

    /* ======================================================================= */
    /*  Helpers                                                                */
    /* ======================================================================= */

    /* ======================================================================= */
    /*                              TREZOR PROTOCOL                            */
    /* ======================================================================= */

    std::shared_ptr<messages::monero::MoneroAddress> device_trezor::get_address(
        const boost::optional<std::vector<uint32_t>> & path,
        const boost::optional<cryptonote::network_type> & network_type){
      AUTO_LOCK_CMD();
      require_connected();
      device_state_reset_unsafe();
      require_initialized();

      auto req = std::make_shared<messages::monero::MoneroGetAddress>();
      this->set_msg_addr<messages::monero::MoneroGetAddress>(req.get(), path, network_type);

      auto response = this->client_exchange<messages::monero::MoneroAddress>(req);
      MTRACE("Get address response received");
      return response;
    }

    std::shared_ptr<messages::monero::MoneroWatchKey> device_trezor::get_view_key(
        const boost::optional<std::vector<uint32_t>> & path,
        const boost::optional<cryptonote::network_type> & network_type){
      AUTO_LOCK_CMD();
      require_connected();
      device_state_reset_unsafe();
      require_initialized();

      auto req = std::make_shared<messages::monero::MoneroGetWatchKey>();
      this->set_msg_addr<messages::monero::MoneroGetWatchKey>(req.get(), path, network_type);

      auto response = this->client_exchange<messages::monero::MoneroWatchKey>(req);
      MTRACE("Get watch key response received");
      return response;
    }

    void device_trezor::ki_sync(wallet_shim * wallet,
                                const std::vector<tools::wallet2::transfer_details> & transfers,
                                hw::device_cold::exported_key_image & ski)
    {
      AUTO_LOCK_CMD();
      require_connected();
      device_state_reset_unsafe();
      require_initialized();

      std::shared_ptr<messages::monero::MoneroKeyImageExportInitRequest> req;

      std::vector<protocol::ki::MoneroTransferDetails> mtds;
      std::vector<protocol::ki::MoneroExportedKeyImage> kis;
      protocol::ki::key_image_data(wallet, transfers, mtds);
      protocol::ki::generate_commitment(mtds, transfers, req);

      this->set_msg_addr<messages::monero::MoneroKeyImageExportInitRequest>(req.get());
      auto ack1 = this->client_exchange<messages::monero::MoneroKeyImageExportInitAck>(req);

      const auto batch_size = 10;
      const auto num_batches = (mtds.size() + batch_size - 1) / batch_size;
      for(uint64_t cur = 0; cur < num_batches; ++cur){
        auto step_req = std::make_shared<messages::monero::MoneroKeyImageSyncStepRequest>();
        auto idx_finish = std::min(static_cast<uint64_t>((cur + 1) * batch_size), static_cast<uint64_t>(mtds.size()));
        for(uint64_t idx = cur * batch_size; idx < idx_finish; ++idx){
          auto added_tdis = step_req->add_tdis();
          CHECK_AND_ASSERT_THROW_MES(idx < mtds.size(), "Invalid transfer detail index");
          *added_tdis = mtds[idx];
        }

        auto step_ack = this->client_exchange<messages::monero::MoneroKeyImageSyncStepAck>(step_req);
        auto kis_size = step_ack->kis_size();
        kis.reserve(static_cast<size_t>(kis_size));
        for(int i = 0; i < kis_size; ++i){
          auto ckis = step_ack->kis(i);
          kis.push_back(ckis);
        }

        MTRACE("Batch " << cur << " / " << num_batches << " batches processed");
      }

      auto final_req = std::make_shared<messages::monero::MoneroKeyImageSyncFinalRequest>();
      auto final_ack = this->client_exchange<messages::monero::MoneroKeyImageSyncFinalAck>(final_req);
      ski.reserve(kis.size());

      for(auto & sub : kis){
        char buff[32*3];
        protocol::crypto::chacha::decrypt(sub.blob().data(), sub.blob().size(),
                                          reinterpret_cast<const uint8_t *>(final_ack->enc_key().data()),
                                          reinterpret_cast<const uint8_t *>(sub.iv().data()), buff);

        ::crypto::signature sig{};
        ::crypto::key_image ki;
        memcpy(ki.data, buff, 32);
        memcpy(sig.c.data, buff + 32, 32);
        memcpy(sig.r.data, buff + 64, 32);
        ski.push_back(std::make_pair(ki, sig));
      }
    }


    void device_trezor::tx_sign(wallet_shim * wallet,
                                const tools::wallet2::unsigned_tx_set & unsigned_tx,
                                tools::wallet2::signed_tx_set & signed_tx,
                                hw::tx_aux_data & aux_data)
    {
      CHECK_AND_ASSERT_THROW_MES(unsigned_tx.transfers.first == 0, "Unsuported non zero offset");
      size_t num_tx = unsigned_tx.txes.size();
      signed_tx.key_images.clear();
      signed_tx.key_images.resize(unsigned_tx.transfers.second.size());

      for(size_t tx_idx = 0; tx_idx < num_tx; ++tx_idx) {
        std::shared_ptr<protocol::tx::Signer> signer;
        tx_sign(wallet, unsigned_tx, tx_idx, aux_data, signer);

        auto & cdata = signer->tdata();
        auto aux_info_cur = signer->store_tx_aux_info();
        aux_data.tx_device_aux.emplace_back(aux_info_cur);

        // Pending tx reconstruction
        signed_tx.ptx.emplace_back();
        auto & cpend = signed_tx.ptx.back();
        cpend.tx = cdata.tx;
        cpend.dust = 0;
        cpend.fee = 0;
        cpend.dust_added_to_fee = false;
        cpend.change_dts = cdata.tx_data.change_dts;
        cpend.selected_transfers = cdata.tx_data.selected_transfers;
        cpend.key_images = "";
        cpend.dests = cdata.tx_data.dests;
        cpend.construction_data = cdata.tx_data;

        // Transaction check
        try {
          transaction_check(cdata, aux_data);
        } catch(const std::exception &e){
          throw exc::ProtocolException(std::string("Transaction verification failed: ") + e.what());
        }

        std::string key_images;
        bool all_are_txin_to_key = std::all_of(cdata.tx.vin.begin(), cdata.tx.vin.end(), [&](const cryptonote::txin_v& s_e) -> bool
        {
          CHECKED_GET_SPECIFIC_VARIANT(s_e, const cryptonote::txin_to_key, in, false);
          key_images += boost::to_string(in.k_image) + " ";
          return true;
        });
        if(!all_are_txin_to_key) {
          throw std::invalid_argument("Not all are txin_to_key");
        }
        cpend.key_images = key_images;

        // KI sync
        size_t num_sources = cdata.tx_data.sources.size();
        CHECK_AND_ASSERT_THROW_MES(num_sources == cdata.source_permutation.size(), "Invalid permutation size");
        CHECK_AND_ASSERT_THROW_MES(num_sources == cdata.tx.vin.size(), "Invalid tx.vin size");
        for(size_t src_idx = 0; src_idx < num_sources; ++src_idx){
          size_t idx_mapped = cdata.source_permutation[src_idx];
          CHECK_AND_ASSERT_THROW_MES(idx_mapped < cdata.tx_data.selected_transfers.size(), "Invalid idx_mapped");
          CHECK_AND_ASSERT_THROW_MES(src_idx < cdata.tx.vin.size(), "Invalid idx_mapped");

          size_t idx_map_src = cdata.tx_data.selected_transfers[idx_mapped];
          auto vini = boost::get<cryptonote::txin_to_key>(cdata.tx.vin[src_idx]);

          CHECK_AND_ASSERT_THROW_MES(idx_map_src < signed_tx.key_images.size(), "Invalid key image index");
          signed_tx.key_images[idx_map_src] = vini.k_image;
        }
      }
    }

    void device_trezor::tx_sign(wallet_shim * wallet,
                   const tools::wallet2::unsigned_tx_set & unsigned_tx,
                   size_t idx,
                   hw::tx_aux_data & aux_data,
                   std::shared_ptr<protocol::tx::Signer> & signer)
    {
      AUTO_LOCK_CMD();
      require_connected();
      device_state_reset_unsafe();
      require_initialized();

      CHECK_AND_ASSERT_THROW_MES(idx < unsigned_tx.txes.size(), "Invalid transaction index");
      signer = std::make_shared<protocol::tx::Signer>(wallet, &unsigned_tx, idx, &aux_data);
      const tools::wallet2::tx_construction_data & cur_tx = unsigned_tx.txes[idx];
      unsigned long num_sources = cur_tx.sources.size();
      unsigned long num_outputs = cur_tx.splitted_dsts.size();

      // Step: Init
      auto init_msg = signer->step_init();
      this->set_msg_addr(init_msg.get());
      transaction_pre_check(init_msg);

      auto response = this->client_exchange<messages::monero::MoneroTransactionInitAck>(init_msg);
      signer->step_init_ack(response);

      // Step: Set transaction inputs
      for(size_t cur_src = 0; cur_src < num_sources; ++cur_src){
        auto src = signer->step_set_input(cur_src);
        auto ack = this->client_exchange<messages::monero::MoneroTransactionSetInputAck>(src);
        signer->step_set_input_ack(ack);
      }

      // Step: sort
      auto perm_req = signer->step_permutation();
      if (perm_req){
        auto perm_ack = this->client_exchange<messages::monero::MoneroTransactionInputsPermutationAck>(perm_req);
        signer->step_permutation_ack(perm_ack);
      }

      // Step: input_vini
      if (!signer->in_memory()){
        for(size_t cur_src = 0; cur_src < num_sources; ++cur_src){
          auto src = signer->step_set_vini_input(cur_src);
          auto ack = this->client_exchange<messages::monero::MoneroTransactionInputViniAck>(src);
          signer->step_set_vini_input_ack(ack);
        }
      }

      // Step: all inputs set
      auto all_inputs_set = signer->step_all_inputs_set();
      auto ack_all_inputs = this->client_exchange<messages::monero::MoneroTransactionAllInputsSetAck>(all_inputs_set);
      signer->step_all_inputs_set_ack(ack_all_inputs);

      // Step: outputs
      for(size_t cur_dst = 0; cur_dst < num_outputs; ++cur_dst){
        auto src = signer->step_set_output(cur_dst);
        auto ack = this->client_exchange<messages::monero::MoneroTransactionSetOutputAck>(src);
        signer->step_set_output_ack(ack);
      }

      // Step: all outs set
      auto all_out_set = signer->step_all_outs_set();
      auto ack_all_out_set = this->client_exchange<messages::monero::MoneroTransactionAllOutSetAck>(all_out_set);
      signer->step_all_outs_set_ack(ack_all_out_set, *this);

      // Step: sign each input
      for(size_t cur_src = 0; cur_src < num_sources; ++cur_src){
        auto src = signer->step_sign_input(cur_src);
        auto ack_sign = this->client_exchange<messages::monero::MoneroTransactionSignInputAck>(src);
        signer->step_sign_input_ack(ack_sign);
      }

      // Step: final
      auto final_msg = signer->step_final();
      auto ack_final = this->client_exchange<messages::monero::MoneroTransactionFinalAck>(final_msg);
      signer->step_final_ack(ack_final);
    }

    void device_trezor::transaction_pre_check(std::shared_ptr<messages::monero::MoneroTransactionInitRequest> init_msg)
    {
      CHECK_AND_ASSERT_THROW_MES(init_msg, "TransactionInitRequest is empty");
      CHECK_AND_ASSERT_THROW_MES(init_msg->has_tsx_data(), "TransactionInitRequest has no transaction data");
      CHECK_AND_ASSERT_THROW_MES(m_features, "Device state not initialized");  // make sure the caller did not reset features
      const bool nonce_required = init_msg->tsx_data().has_payment_id() && init_msg->tsx_data().payment_id().size() > 0;

      if (nonce_required){
        // Versions 2.0.9 and lower do not support payment ID
        CHECK_AND_ASSERT_THROW_MES(m_features->has_major_version() && m_features->has_minor_version() && m_features->has_patch_version(), "Invalid Trezor firmware version information");
        const uint32_t vma = m_features->major_version();
        const uint32_t vmi = m_features->minor_version();
        const uint32_t vpa = m_features->patch_version();
        if (vma < 2 || (vma == 2 && vmi == 0 && vpa <= 9)) {
          throw exc::TrezorException("Trezor firmware 2.0.9 and lower does not support transactions with short payment IDs or integrated addresses. Please update.");
        }
      }
    }

    void device_trezor::transaction_check(const protocol::tx::TData & tdata, const hw::tx_aux_data & aux_data)
    {
      // Simple serialization check
      cryptonote::blobdata tx_blob;
      cryptonote::transaction tx_deserialized;
      bool r = cryptonote::t_serializable_object_to_blob(tdata.tx, tx_blob);
      CHECK_AND_ASSERT_THROW_MES(r, "Transaction serialization failed");
      r = cryptonote::parse_and_validate_tx_from_blob(tx_blob, tx_deserialized);
      CHECK_AND_ASSERT_THROW_MES(r, "Transaction deserialization failed");

      // Extras check
      std::vector<cryptonote::tx_extra_field> tx_extra_fields;
      cryptonote::tx_extra_nonce nonce;

      r = cryptonote::parse_tx_extra(tdata.tx.extra, tx_extra_fields);
      CHECK_AND_ASSERT_THROW_MES(r, "tx.extra parsing failed");

      const bool nonce_required = tdata.tsx_data.has_payment_id() && tdata.tsx_data.payment_id().size() > 0;
      const bool has_nonce = cryptonote::find_tx_extra_field_by_type(tx_extra_fields, nonce);
      CHECK_AND_ASSERT_THROW_MES(has_nonce == nonce_required, "Transaction nonce presence inconsistent");

      if (nonce_required){
        const std::string & payment_id = tdata.tsx_data.payment_id();
        if (payment_id.size() == 32){
          crypto::hash payment_id_long{};
          CHECK_AND_ASSERT_THROW_MES(cryptonote::get_payment_id_from_tx_extra_nonce(nonce.nonce, payment_id_long), "Long payment ID not present");

        } else if (payment_id.size() == 8){
          crypto::hash8 payment_id_short{};
          CHECK_AND_ASSERT_THROW_MES(cryptonote::get_encrypted_payment_id_from_tx_extra_nonce(nonce.nonce, payment_id_short), "Short payment ID not present");
        }
      }
    }

#else //WITH_DEVICE_TREZOR

    void register_all(std::map<std::string, std::unique_ptr<device>> &registry) {
    }

    void register_all() {
    }

#endif //WITH_DEVICE_TREZOR
}}
