/** Copyright (c) 2020-2021 The Creators of Simphone

    class ContactInfo (QDialog): display information about a contact in real-time

    See the file COPYING.LESSER.txt for copying permission.
**/

#include "contactinfo.h"
#include "ui_contactinfo.h"

#include "qtfix.h"
#include "contacts.h"

#include <QClipboard>
#include <QMenu>
#include <QKeyEvent>

ContactInfo::ContactInfo(QWidget * parent, Contact * contact)
  : QDialog(parent)
  , ui(new Ui::ContactInfo)
  , m_contact(contact)
  , m_nickRow(-1)
  , m_settingsRow(-1)
  , m_statsRow(-1)
{
  log_info_("ui", "create contact info %s\n", contact->m_nick.toUtf8().data());
  ui->setupUi(this);
  ui->okButton->setText(qApp->translate("QDialogButtonBox", "OK"));
  setFont(qtfix::fixFontSize(font(), "QAbstractItemView")); //ui->okButton
  setAttribute(Qt::WA_DeleteOnClose);
  setWindowFlags(Qt::WindowMinimizeButtonHint | Qt::WindowSystemMenuHint | Qt::WindowCloseButtonHint);
  onTimerTimeout(true);

  int hs = ui->infoTable->horizontalHeader()->sectionSize(0) + ui->infoTable->horizontalHeader()->sectionSize(1);
  int vs = ui->infoTable->verticalHeader()->sectionSize(0) * (ui->infoTable->rowCount() + 2);
  resize(hs + ui->infoTable->horizontalHeader()->sectionSize(2) + 8, vs + ui->okButton->sizeHint().height() + 8);
  connect(&m_timer, SIGNAL(timeout()), this, SLOT(onTimerTimeout()));
  connect(Contacts::get(), SIGNAL(signalLogout()), this, SLOT(onSignalLogout()));
  connect(ui->infoTable, SIGNAL(doubleClicked(const QModelIndex &)),
          this, SLOT(onItemDoubleClicked(const QModelIndex &)));
  m_timer.start(1000);

  ui->infoTable->setContextMenuPolicy(Qt::CustomContextMenu);
  connect(ui->infoTable, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(onCustomContextMenu(const QPoint &)));
  installEventFilter(this);
  restorePosition();
}

ContactInfo::~ContactInfo()
{
  removeEventFilter(this);
  if (m_contact) m_contact->setContactInfo(0, pos().x(), pos().y());

  delete ui;
  if (m_contact) log_info_("ui", "delete contact info %s\n", m_contact->m_nick.toUtf8().data());
}

void ContactInfo::restorePosition()
{
  int posX = -1, posY = -1;

  if (m_contact) {
    m_contact->getContactInfo(&posX, &posY);
    m_contact->setContactInfo(this, posX, posY);
  }
  if (posX > 0 || posY > 0) move(posX, posY);
}

void ContactInfo::copySelection()
{
  QString qs;
  QItemSelectionModel * selection = ui->infoTable->selectionModel();
  QModelIndexList selected = selection->selectedIndexes();

  std::sort(selected.begin(), selected.end());
  for (int i = 0; i < selected.size(); ++i) {
    QModelIndex index = selected.at(i);
    if (!index.column()) {
      QVariant name = ui->infoTable->item(index.row(), 0)->data(Qt::DisplayRole);
      QVariant var = ui->infoTable->item(index.row(), 2)->data(Qt::DisplayRole);
      if (!name.toString().isEmpty()) qs.append(name.toString()).append(": ");
      qs.append(var.toString()).append("\n");
    }
  }
  QApplication::clipboard()->setText(qs);
}

void ContactInfo::showEvent(QShowEvent * event)
{
  restorePosition();
  Parent::showEvent(event);
}

void ContactInfo::hideEvent(QHideEvent * event)
{
  if (m_contact) m_contact->setContactInfo(this, pos().x(), pos().y());
  Parent::hideEvent(event);
}

bool ContactInfo::eventFilter(QObject * obj, QEvent * event)
{
  if (event->type() == QEvent::KeyPress) {
    QKeyEvent * keyEvent = static_cast<QKeyEvent *>(event);

    if (keyEvent->matches(QKeySequence::Copy) ||
        (keyEvent->modifiers() & Qt::ControlModifier && keyEvent->key() == Qt::Key_C)) {
      copySelection();
      return true;
    }

    if (keyEvent->modifiers() & Qt::ControlModifier && keyEvent->key() == Qt::Key_A) {
      ui->infoTable->selectAll();
      return true;
    }
  }
  return Parent::eventFilter(obj, event);
}

void ContactInfo::on_okButton_clicked()
{
  m_timer.stop();
  accept();
}

void ContactInfo::onItemDoubleClicked(const QModelIndex & index)
{
  //QVariant var = ui->infoTable->model()->data(index); // QAbstractItemView::SelectItems
  QVariant var = ui->infoTable->item(index.row(), 2)->data(Qt::DisplayRole);
  if (SimParam::get("ui.chat.doubleclick")) QApplication::clipboard()->setText(var.toString());
}

void ContactInfo::onTimerTimeout(bool first)
{
  if (!m_contact) return;

  QString nick = m_contact->getNickToolTip(false);
  QString nick0 = m_contact->getNickToolTip(true);
  simtype contact = sim_contact_get_(m_contact->m_simId, CONTACT_BIT_DEFAULT | CONTACT_BIT_LATENCY | CONTACT_BIT_STATS);
  QPalette pal = ui->infoTable->palette();

  pal.setColor(QPalette::Base, palette().color(QPalette::AlternateBase));
  ui->infoTable->setPalette(pal);
  ui->infoTable->setColumnCount(3);
  if (sim_get_pointer(contact)) {
    ui->infoTable->setRowCount(13 + 4 /* stats */);

    bool pinged = false;
    int row = 0, status;
    simnumber n, n0, n1, n2, n3, n4;
    double d0, d1;
    simtype s = sim_table_get_string(contact, CONTACT_KEY_ADDRESS), stats;
    QString cs, qs, qs0, qs1, qs2, qs3, qs4;
    char buff[64];
    Contact::E_State state;

    if (!sim_get_pointer(s)) {
      s = sim_pointer_new("                                                                          ");
    }
    setInfo(row, tr("Address"), sim_get_pointer(s),
            m_contact->isMe() || m_contact->isTest()
              ? tr("This is your public Simphone address.")
              : tr("This is %1's public Simphone address.").arg(nick));

    setInfo(++row, tr("Identifier"), m_contact->getTextId(),
            m_contact->isTest()
              ? ""
              : m_contact->isMe()
                  ? tr("This is the name of the file that contains notes you wrote to yourself.")
                  : tr("This is the name of the chat history file with %1.").arg(nick));

    status = m_contact->isMe() ? SimCore::getMyStatus() : sim_table_get_number(contact, CONTACT_KEY_STATUS);
    state = m_contact->convertStatusToState(short(status == SIM_STATUS_IDLE ? SIM_STATUS_AWAY : status));
    qs0 = m_contact->getStateToolTip(state);
    cs = tr("Status");
    switch (status) {
      default: setInfo(++row, cs, "", qs0); break;
      case SIM_STATUS_OFF: setInfo(++row, cs, tr("Offline (off)"), qs0); break;
      case SIM_STATUS_ON: setInfo(++row, cs, tr("Online (on)"), qs0); break;
      case SIM_STATUS_IDLE:
      case SIM_STATUS_AWAY: setInfo(++row, cs, tr("Away From Keyboard (idle)"), qs0); break;
      case SIM_STATUS_BUSY: setInfo(++row, cs, tr("Do Not Disturb (busy)"), qs0); break;
      case SIM_STATUS_HIDE: setInfo(++row, cs, tr("Not Visible (hiding)"), qs0); break;
      case SIM_STATUS_INVISIBLE: setInfo(++row, cs, m_contact->isMe() ? tr("Invisible") : tr("Offline (gone)"), qs0);
    }

    if (m_contact->isTest()) {
      unsigned flags = 0;

      setInfo(++row, "", "", "");

      cs = m_contact->convertLocationToCountry(sim_table_get_pointer(contact, CONTACT_KEY_LOCATION), 0);
      if (!sim_get_length(s = sim_table_get_string(contact, CONTACT_KEY_CONNECT))) {
        qs = Contact::tr("none", "proxy");
      } else {
        qs = qApp->translate("SimCountry", cs.toUtf8().data());
        qs = cs.isEmpty() || cs == Contact::tr("unknown") ? sim_get_pointer(s) : qs + " (" + sim_get_pointer(s) + ")";
      }
      n = sim_table_get_number(contact, CONTACT_KEY_LATENCY);
      if (n && !first) qs.append(tr(" - ping %1 ms").arg(n));
      setInfo(++row, tr("Used proxy"), qs,
              !sim_get_length(s)
                ? tr("You are not currently connected to a proxy.")
                : n
                    ? tr("You are currently connected to a proxy at this internet address.")
                    : tr("You are currently connected to a proxy but the connection does not seem to respond."),
              cs);

      qs0 = sim_get_length(s = sim_table_get_string(contact, CONTACT_KEY_IP)) ? sim_get_pointer(s) : tr("unknown");
      qs = m_contact->convertLocationToCountry(sim_get_pointer(s), 1);

      stats = sim_list_info_(SIM_INFO_BIT_NET);
      s = sim_table_get_string(stats, SIM_INFO_NET_IP);
      if (sim_get_length(s) && qs0 != sim_get_pointer(s)) qs0.append("/").append(sim_get_pointer(s));
      setInfo(++row, tr("IP address"),
              qs.isEmpty() || qs == Contact::tr("unknown")
                ? qs0
                : qApp->translate("SimCountry", qs.toUtf8().data()) + " (" + qs0 + ")",
              (qs.isEmpty() || qs == Contact::tr("unknown")) && qs0 == tr("unknown")
                ? tr("Your internet address is not yet known.")
                : tr("This is your own internet address to the best knowledge of Simphone."),
              qs);

      SimCore::getMyStatus(&flags);
      if (flags & SIM_STATUS_FLAG_DHT_OUT) {
        qs0 = tr("You are connected to the internet.");
      } else if (!(flags & (SIM_STATUS_FLAG_TCP_OUT | SIM_STATUS_FLAG_SSL_OUT |
                            SIM_STATUS_FLAG_UDP_OUT | SIM_STATUS_FLAG_DNS))) {
        qs0 = tr("You do not seem to be connected to the internet.");
      } else if (!(flags & SIM_STATUS_FLAG_UDP_OUT)) {
        qs0 = tr("You do not have UDP connectivity. Peer-to-peer does not work.");
      } else {
        qs0 = tr("You seem to have UDP connectivity but peer-to-peer does not yet work.");
      }
      if (!(flags & SIM_STATUS_FLAG_TCP_OUT)) {
        if (flags & SIM_STATUS_FLAG_TCP_IN) qs0.append("\n").append(tr("You have incoming internet connectivity."));
        if (flags & SIM_STATUS_FLAG_SSL_OUT) qs0.append("\n").append(tr("You can connect to web servers."));
      } else if (flags & SIM_STATUS_FLAG_TCP_IN) {
        qs0.append("\n").append(tr("You have full internet connectivity."));
      } else {
        qs0.append("\n").append(tr("You have outgoing internet connectivity."));
      }
      if (flags & SIM_STATUS_FLAG_DNS) qs0.append("\n").append(tr("Your DNS resolver seems to have worked."));
      if (flags & SIM_STATUS_FLAG_UPNP) qs0.append("\n").append(tr("Your incoming port has been forwarded over UPnP."));
      if (flags & SIM_STATUS_FLAG_UDP_IN) {
        if (flags & SIM_STATUS_FLAG_DHT_IN) {
          qs0.append("\n").append(tr("You also have incoming UDP connectivity."));
        } else {
          qs0.append("\n").append(tr("You seem to have incoming UDP connectivity."));
        }
      }
      qs = getFlagNames(flags & SIM_STATUS_FLAG_DHT_IN, flags & SIM_STATUS_FLAG_DHT_OUT, "DHT", "in", "out");
      if (flags & SIM_STATUS_FLAG_DHT_IN) flags &= ~SIM_STATUS_FLAG_UDP_IN;
      if (flags & SIM_STATUS_FLAG_DHT_OUT) flags &= ~SIM_STATUS_FLAG_UDP_OUT;
      if (flags & (SIM_STATUS_FLAG_UDP_IN | SIM_STATUS_FLAG_UDP_OUT)) {
        qs += getFlagNames(flags & SIM_STATUS_FLAG_UDP_IN, flags & SIM_STATUS_FLAG_UDP_OUT, "UDP", "in", "out");
      }
      qs += getFlagNames(flags & SIM_STATUS_FLAG_TCP_IN, flags & SIM_STATUS_FLAG_TCP_OUT, "TCP", "in", "out");
      qs += getFlagNames(flags & SIM_STATUS_FLAG_SSL_IN, flags & SIM_STATUS_FLAG_SSL_OUT, "SSL", "in", "out");
      qs += flags & SIM_STATUS_FLAG_UPNP ? ", UPNP" : "";
      qs += flags & SIM_STATUS_FLAG_DNS ? ", DNS" : "";
      qs = qs.isEmpty() ? tr("none", "flags") : qs.mid(2);
      if (!(flags & SIM_STATUS_FLAG_DHT_OUT)) {
        setInfo(++row, tr("Connection"), qs, qs0, QColor(Qt::red));
      } else if (flags & SIM_STATUS_FLAG_TCP_OUT) {
        setInfo(++row, tr("Connection"), qs, qs0);
      } else {
        setInfo(++row, tr("Connection"), qs, qs0, QColor(Qt::darkMagenta));
      }

      int goodNodes = sim_table_get_number(stats, SIM_INFO_NET_DHT_GOOD);
      int dubiousNodes = sim_table_get_number(stats, SIM_INFO_NET_DHT_DUBIOUS);
      int blockedNodes = sim_table_get_number(stats, SIM_INFO_NET_DHT_BLOCKED);
      int badNodes = sim_table_get_number(stats, SIM_INFO_NET_DHT_BAD);

      qs = tr("%1 nodes").arg(goodNodes);
      if (dubiousNodes) qs = tr("%1 + %2 nodes").arg(goodNodes).arg(dubiousNodes);
      if (blockedNodes) {
        qs.append(tr(", %1 + %2 blocked").arg(blockedNodes).arg(badNodes));
      } else if (badNodes) {
        qs.append(tr(", %1 blocked").arg(badNodes));
      }
      sim_list_free(stats);

      if (goodNodes < 4 || goodNodes + dubiousNodes <= 8 || (goodNodes < 20 && goodNodes * 8 < dubiousNodes)) {
        setInfo(++row, tr("DHT size"), qs, tr("You are not connected to the DHT network."), QColor(Qt::red));
      } else if (goodNodes < 40) {
        setInfo(++row, tr("DHT size"), qs, tr("You are connecting to the DHT network."), QColor(Qt::darkMagenta));
      } else {
        setInfo(++row, tr("DHT size"), qs, tr("You are connected to the DHT network."));
      }
    } else {
#ifdef _DEBUG
      if (sim_audio_check_talking_() == m_contact->m_simId && !SimCore::get()->isPinging()) {
        sim_contact_ping_(m_contact->m_simId, 0, SIM_PING_BIT_AUDIO);
        pinged = true;
      }
#endif

      s = sim_table_get_string(contact, CONTACT_KEY_OWN_NICK);
      setInfo(m_nickRow = ++row, tr("Own nick"), sim_get_pointer(s),
              m_contact->isMe()
                ? tr("You can change your nickname by right-clicking here.")
                : sim_get_length(s)
                    ? tr("%1 calls himself %2.").arg(nick0, QString(sim_get_pointer(s)).toHtmlEscaped())
                    : tr("Not yet known how %1 calls himself.").arg(nick));

      s = sim_table_get_array_string(contact, CONTACT_KEY_HOSTS);
      for (unsigned i = 1; i <= sim_get_length(s); i++) {
        if (i > 1) qs.append(tr(", "));
        qs.append(sim_array_get_pointer(s, i));
      }
      if (qs.isEmpty()) {
        setInfo(++row, tr("Dynamic DNS"), tr("none", "dns"),
                !m_contact->isMe()
                  ? tr("%1 does not use DynDNS.").arg(nick0)
                  : tr("You can set your DynDNS hostname from the \"Network\" tab of \"Settings\"."));
      } else {
        setInfo(++row, tr("Dynamic DNS"), qs,
                tr("Simphone will try to connect to %1 at this host and port.").arg(nick), QColor(Qt::darkGreen));
      }

      qs0 = sim_get_pointer(s = sim_table_get_string(contact, CONTACT_KEY_LOCATION));
      cs = m_contact->convertLocationToCountry(sim_get_pointer(s), 0);
      qs1 = sim_get_pointer(s = sim_table_get_string(contact, CONTACT_KEY_IP));
      n = sim_table_get_number(contact, CONTACT_KEY_LATENCY);
      qs = qApp->translate("SimCountry", cs.toUtf8().data());
      qs = cs.isEmpty() ? (sim_get_length(s) ? sim_get_pointer(s) : n ? Contact::tr("unknown") : "")
                        : sim_get_length(s) ? qs + " (" + sim_get_pointer(s) + ")" : qs;
      if (n) qs.append(tr(" - ping %1 ms").arg(n));
      if (sim_get_length(s = sim_table_get_string(contact, CONTACT_KEY_CONNECT))) {
        qs2 = !n ? tr("You are currently connected to %1 but the connection does not seem to respond.").arg(nick)
                 : !qs1.isEmpty()
                     ? tr("You are currently connected to %1 at this internet address.").arg(nick)
                     : tr("You are currently connected to %1.").arg(nick);
      } else if (!m_contact->isMe()) {
        qs2 = cs.isEmpty() ? tr("You have never connected to %1.").arg(nick)
                           : tr("%1 was last seen at %2").arg(nick0, qs0);
      } else {
        qs2 = tr("You can see your location from the \"Info\", after right-clicking on the user \"test\".");
        if (!cs.isEmpty()) qs2 = tr("Your OTHER location was at %1.").arg(qs0);
      }
      setInfo(++row, tr("Location"), qs, qs2, cs);

      if (!sim_get_length(s)) {
        if (m_contact->isMe()) {
          qs0 = tr("You cannot connect to yourself.");
        } else {
          qs0 = tr("You are not currently connected to %1.").arg(nick);
        }
      } else if (!strcmp(sim_get_pointer(s), "OUT")) {
        qs0 = tr("You have connected directly to %1.").arg(nick);
      } else if (!strcmp(sim_get_pointer(s), "IN")) {
        qs0 = tr("%1 has connected directly to you.").arg(nick0);
      } else if (!strcmp(sim_get_pointer(s), "REVERSE")) {
        qs0 = tr("%1 has reversed your relayed connection and has connected directly to you.").arg(nick0);
      } else if (!strcmp(sim_get_pointer(s), "DIRECT")) {
        qs0 = tr("You have reversed %1's relayed connection and have connected directly.").arg(nick);
      } else if (!strcmp(sim_get_pointer(s), "LOCAL")) {
        qs0 = tr("You have reversed %1's relayed connection and have connected directly over your local network.");
        qs0 = qs0.arg(nick);
      } else if (sim_get_length(s) == SIM_STRING_GET_LENGTH_CONST("OUTx") &&
                 !SIM_STRING_CHECK_DIFF_CONST(sim_get_pointer(s), "OUT")) {
        qs0 = tr("You have connected directly to %1 after successful NAT traversal.").arg(nick);
      } else if (!strcmp(sim_get_pointer(s), "TRAVERSE")) {
        qs0 = tr("%1 has connected directly to you after successful NAT traversal.").arg(nick0);
      } else if (strchr(sim_get_pointer(s), '/') && !m_contact->hasTraversed()) {
        qs0 = tr("Simphone is now attempting to perform NAT traversal to connect you directly to %1.").arg(nick);
      } else if (!SIM_STRING_CHECK_DIFF_CONST(sim_get_pointer(s), "IN/") ||
                 !SIM_STRING_CHECK_DIFF_CONST(sim_get_pointer(s), "OUT/")) {
        bool ok = true;
        for (char * p = strchr(sim_get_pointer(s), '/') + 1; *p; p++) {
          if ((*p < '0' || *p > '9') && (*p != '.')) ok = false;
        }
        if (ok && strlen(strchr(sim_get_pointer(s), '/') + 1)) {
          if (!SIM_STRING_CHECK_DIFF_CONST(sim_get_pointer(s), "IN/")) {
            qs0 = tr("%1 has connected to you through your proxy at %2").arg(nick0, sim_get_pointer(s) + 3);
          } else {
            qs0 = tr("You have connected to %1 through a proxy at %2").arg(nick, sim_get_pointer(s) + 4);
          }
        } else if (!SIM_STRING_CHECK_DIFF_CONST(sim_get_pointer(s), "IN/")) {
          qs0 = tr("%1 has somehow connected to you.").arg(nick0);
        } else {
          qs0 = tr("You have somehow connected to %1.").arg(nick);
        }
      } else {
        qs0 = tr("%1 and you are somehow connected to each other.").arg(nick0);
      }

      if (sim_get_pointer(s) && strchr(sim_get_pointer(s), '/')) {
        qs = m_contact->convertLocationToCountry(strchr(sim_get_pointer(s), '/') + 1, 1);
      } else {
        qs = QString();
      }
      setInfo(++row, tr("Connection"),
              !sim_get_length(s)
                ? tr("none", "connection")
                : qs.isEmpty() || qs == Contact::tr("unknown")
                    ? sim_get_pointer(s)
                    : QString(sim_get_pointer(s)) + " (" + qApp->translate("SimCountry", qs.toUtf8().data()) + ")",
              qs0, qs);

      n = sim_table_get_number(contact, CONTACT_KEY_KEY_SIZE);
      setInfo(++row, tr("Key size"), n ? tr("%1 bits").arg(n) : "",
              m_contact->isMe()
                ? tr("You have a %1-bit RSA key.").arg(n)
                : n
                    ? tr("%1 has a %2-bit RSA key.").arg(nick0).arg(n)
                    : tr("%1's key size is not known yet.").arg(nick0));
    }

    sim_convert_time_to_string(n = sim_table_get_number(contact, CONTACT_KEY_SEEN), buff);
    setInfo(++row, tr("Last seen"), n ? buff : "",
            !n
              ? m_contact->isTest()
                  ? tr("You do not have an audio device.")
                  : m_contact->isMe()
                      ? tr("Simphone has never seen you at another computer.")
                      : tr("Simphone has never seen %1.").arg(nick)
              : m_contact->isTest()
                  ? tr("Date and time when Simphone has last seen your audio device.")
                  : m_contact->isMe()
                      ? tr("Date and time when Simphone has last seen you at another computer.")
                      : tr("Date and time when Simphone has last seen %1.").arg(nick));

    if (!m_contact->m_badCryptoName.isEmpty()) {
      qs = tr("%1 uses an undesired cipher. Please, do agree on a common cipher to use.");
      setInfo(++row, tr("Undesired cipher"), m_contact->m_badCryptoName, qs.arg(nick0), QColor(Qt::darkMagenta));
    } else if (!m_contact->isTest()) {
      s = sim_table_get(contact, CONTACT_KEY_CIPHER);
      setInfo(++row, tr("Used cipher"), sim_get_pointer(s),
              !sim_get_length(s)
                ? m_contact->isMe()
                    ? tr("You can set your preferred ciphers from the \"Security\" tab of \"Settings\".")
                    : tr("No cipher has been used with %1 yet.").arg(nick)
                : tr("This is the cipher you and %1 have last used.").arg(nick));
    } else {
      setInfo(++row, "", "", "");
    }

    stats = m_contact->isMe() ? sim_contact_get_(0, CONTACT_BIT_STATS) : sim_nil();

    nick = m_contact->isMe() ? tr("<b>yourself</b>") : m_contact->isTest() ? tr("<b>the DHT</b>") : nick;
    if ((m_contact->isMe() || m_contact->isTest()) && (nick.isEmpty() || nick == " ")) {
      nick = m_contact->m_nick.toHtmlEscaped();
    }
    n0 = sim_table_get_number(contact, CONTACT_KEY_ALL_CLIENT_TIME);
    n1 = sim_table_get_number(contact, CONTACT_KEY_THIS_CLIENT_TIME) / 1000;
    n = sim_table_get_number(contact, CONTACT_KEY_ALL_CLIENT_COUNT);
    n2 = sim_table_get_number(contact, CONTACT_KEY_THIS_CLIENT_COUNT);
    if (m_contact->isMe()) {
      n0 += sim_table_get_number(stats, CONTACT_KEY_ALL_CLIENT_TIME);
      n += sim_table_get_number(stats, CONTACT_KEY_ALL_CLIENT_COUNT);
    }
    n0 /= 1000;
    qs0 = tr("Connected %n time(s) to %1 for %2, since Simphone started.", 0, int(n2));
    qs0 = qs0.arg(nick, Contact::convertTimeToText(n1));
    qs1 = tr("Previously connected %n time(s) to %1 for %2.", 0, int(n)).arg(nick, Contact::convertTimeToText(n0));
    qs = tr("old: %1 (%2), new: %3 (%4)").arg(n).arg(Contact::convertTimeToString(n0)).arg(n2);
    setInfo(++row, tr("Sessions"), qs.arg(Contact::convertTimeToString(n1)), qs0.append("\n").append(qs1));

    bool rate = SimParam::get(m_contact->isTest() ? "ui.console.statistics" : "ui.chat.statistics") != 0;

    n3 = n4 = sim_table_get_number(contact, CONTACT_KEY_ALL_CLIENT_RECEIVED);
    if (m_contact->isMe()) {
      n4 = n3 += sim_table_get_number(stats, CONTACT_KEY_ALL_CLIENT_RECEIVED);
      n3 += sim_table_get_number(stats, CONTACT_KEY_ALL_SOCKET_RECEIVED);
      n3 += sim_table_get_number(stats, CONTACT_KEY_ALL_OUTPUT_RECEIVED);
    }
    n = n0 ? n4 / n0 : 0;
    n2 = n1 ? sim_table_get_number(contact, CONTACT_KEY_THIS_CLIENT_RECEIVED) / n1 : 0;
    qs0.sprintf("%.3f", double(n) / 1024.0);
    qs.sprintf("%.3f", double(n2) / 1024.0);
    if (rate) {
      qs = tr("old: %1 KBPS, new: %2 KBPS").arg(qs0, qs);
    } else {
      qs1 = tr("Download speed from %1: %2 KBPS, since Simphone started.").arg(nick, qs).append("\n");
      qs2 = tr("Previous average download speed from %1: %2 KBPS.").arg(nick, qs0);
    }
    n = n3;
    n2 = sim_table_get_number(contact, CONTACT_KEY_THIS_CLIENT_RECEIVED);
    n3 = sim_table_get_number(contact, CONTACT_KEY_ALL_INPUT_RECEIVED);
    n4 = sim_table_get_number(contact, CONTACT_KEY_THIS_INPUT_RECEIVED);
    if (m_contact->isMe()) n3 += sim_table_get_number(stats, CONTACT_KEY_ALL_INPUT_RECEIVED);
    n3 += sim_table_get_number(contact, CONTACT_KEY_ALL_OUTPUT_RECEIVED);
    n4 += sim_table_get_number(contact, CONTACT_KEY_THIS_OUTPUT_RECEIVED);
    n3 += sim_table_get_number(contact, CONTACT_KEY_ALL_SOCKET_RECEIVED);
    n4 += sim_table_get_number(contact, CONTACT_KEY_THIS_SOCKET_RECEIVED);
    cs.sprintf("%.3f", double(n >> 10) / 1024.0);
    qs0.sprintf("%.3f", double(n2 >> 10) / 1024.0);
    qs3.sprintf("%.3f", double(n3 >> 10) / 1024.0);
    qs4.sprintf("%.3f", double(n4 >> 10) / 1024.0);
    if (rate) {
      qs1 = tr("Downloaded %1+%2 megabytes from %3, since Simphone started.").arg(qs0, qs4, nick).append("\n");
      qs2 = tr("Previously downloaded %1+%2 megabytes from %3.").arg(cs, qs3, nick);
    } else {
      qs = tr("old: %1+%2 MB, new: %3+%4 MB").arg(cs, qs3, qs0, qs4);
    }
    setInfo(++row, tr("Received"), qs, qs1.append(qs2));

    n3 = n4 = sim_table_get_number(contact, CONTACT_KEY_ALL_CLIENT_SENT);
    if (m_contact->isMe()) {
      n4 = n3 += sim_table_get_number(stats, CONTACT_KEY_ALL_CLIENT_SENT);
      n3 += sim_table_get_number(stats, CONTACT_KEY_ALL_SOCKET_SENT);
      n3 += sim_table_get_number(stats, CONTACT_KEY_ALL_OUTPUT_SENT);
    }
    n = n0 ? n4 / n0 : 0;
    n2 = n1 ? sim_table_get_number(contact, CONTACT_KEY_THIS_CLIENT_SENT) / n1 : 0;
    qs0.sprintf("%.3f", double(n) / 1024.0);
    qs.sprintf("%.3f", double(n2) / 1024.0);
    if (rate) {
      qs = tr("old: %1 KBPS, new: %2 KBPS").arg(qs0, qs);
    } else {
      qs1 = tr("Upload speed to %1: %2 KBPS, since Simphone started.").arg(nick, qs).append("\n");
      qs2 = tr("Previous average upload speed to %1: %2 KBPS.").arg(nick, qs0);
    }
    n = n3;
    n2 = sim_table_get_number(contact, CONTACT_KEY_THIS_CLIENT_SENT);
    n3 = sim_table_get_number(contact, CONTACT_KEY_ALL_INPUT_SENT);
    n4 = sim_table_get_number(contact, CONTACT_KEY_THIS_INPUT_SENT);
    if (m_contact->isMe()) n3 += sim_table_get_number(stats, CONTACT_KEY_ALL_INPUT_SENT);
    n3 += sim_table_get_number(contact, CONTACT_KEY_ALL_OUTPUT_SENT);
    n4 += sim_table_get_number(contact, CONTACT_KEY_THIS_OUTPUT_SENT);
    n3 += sim_table_get_number(contact, CONTACT_KEY_ALL_SOCKET_SENT);
    n4 += sim_table_get_number(contact, CONTACT_KEY_THIS_SOCKET_SENT);
    cs.sprintf("%.3f", double(n >> 10) / 1024.0);
    qs0.sprintf("%.3f", double(n2 >> 10) / 1024.0);
    qs3.sprintf("%.3f", double(n3 >> 10) / 1024.0);
    qs4.sprintf("%.3f", double(n4 >> 10) / 1024.0);
    if (rate) {
      qs1 = tr("Uploaded %1+%2 megabytes to %3, since Simphone started.").arg(qs0, qs4, nick).append("\n");
      qs2 = tr("Previously uploaded %1+%2 megabytes to %3.").arg(cs, qs3, nick);
    } else {
      qs = tr("old: %1+%2 MB, new: %3+%4 MB").arg(cs, qs3, qs0, qs4);
    }
    setInfo(++row, tr("Sent"), qs, qs1.append(qs2));

    sim_contact_free_(stats);

    if (m_contact->isTest()) {
      nick = tr("<b>your audio test</b>");
      nick0 = tr("<b>Your audio test</b>");
      if (nick.isEmpty() || nick == " ") nick = "<b>test</b>";
      if (nick0.isEmpty() || nick0 == " ") nick0 = "<b>test</b>";
    }
    n = sim_table_get_number(contact, CONTACT_KEY_FRAMES_MINE_ALL);
    n0 = sim_table_get_number(contact, CONTACT_KEY_FRAMES_MINE_WRITE);
    n1 = sim_table_get_number(contact, CONTACT_KEY_FRAMES_MINE_READ);
    n2 = sim_table_get_number(contact, CONTACT_KEY_FRAMES_MINE_LOST);
    qs1 = tr(", %1 unread").arg(qs0.sprintf("%.2f%%", d1 = n ? double(n1) * 100 / n : 0));
    qs2 = tr(", %1 unwritten").arg(qs0.sprintf("%.2f%%", d0 = n ? double(n0) * 100 / n : 0));
    qs0.sprintf("%.2f%%", n ? double(n2) * 100 / n : 0);
    qs = tr("%1 lost").arg(qs0).append(n && n1 ? qs1 : "").append(n && n0 ? qs2 : "");
    qs0 = qs1 = "";
    if (n0) qs0 = tr("%1 audio frames from %2 could not be played.").arg(n0).arg(nick).append("\n");
    if (n1) qs1 = tr("%1 audio frames to %2 could not be recorded.").arg(n1).arg(nick).append("\n");
    qs2 = tr("%1 out of %2 audio frames from %3 were lost.").arg(n2).arg(n).arg(nick);
    n = sim_table_get_number(contact, CONTACT_KEY_MS_TO_CLIENT);
    if (n && pinged) qs.append(tr(" - ping %1 ms").arg(n));
    setInfo(m_statsRow = ++row, tr("Audio (local)"), qs, qs0.append(qs1).append(qs2));

    n = sim_table_get_number(contact, CONTACT_KEY_FRAMES_PEER_ALL);
    if (n && sim_audio_check_talking_() == m_contact->m_simId) {
      n0 = sim_table_get_number(contact, CONTACT_KEY_FRAMES_PEER_WRITE);
      n1 = sim_table_get_number(contact, CONTACT_KEY_FRAMES_PEER_READ);
      n2 = sim_table_get_number(contact, CONTACT_KEY_FRAMES_PEER_LOST);
      qs1 = tr(", %1 unread").arg(qs0.sprintf("%.2f%%", d1 = double(n1) * 100 / n));
      qs2 = tr(", %1 unwritten").arg(qs0.sprintf("%.2f%%", d0 = double(n0) * 100 / n));
      qs = tr("%1 lost").arg(qs0.sprintf("%.2f%%", double(n2) * 100 / n)).append(n1 ? qs1 : "").append(n0 ? qs2 : "");

      qs0 = qs1 = "";
      if (n0) qs0 = tr("%1 could not play %2 audio frames from you.").arg(nick0).arg(n0).append("\n");
      if (n1) qs1 = tr("%1 could not record %2 audio frames for you.").arg(nick0).arg(n1).append("\n");
      qs2 = tr("%1 has lost %2 out of %3 audio frames from you.").arg(nick0).arg(n2).arg(n);
      n = sim_table_get_number(contact, CONTACT_KEY_MS_FROM_CLIENT);
      if (n && pinged) qs.append(tr(" - pong %1 ms").arg(n));
      setInfo(++row, tr("Audio (remote)"), qs, qs0.append(qs1).append(qs2));
    } else if (!m_contact->m_badSystemName.isEmpty()) {
      qs0 = tr("%1 is using an operating system that might record all communication.").arg(nick0);
      if (m_contact->isMe()) qs0 = "";
      setInfo(++row, tr("Insecure system"), m_contact->m_badSystemName, qs0, QColor(Qt::red));
    } else {
      setInfo(++row, "", "", "");
    }

    n = sim_table_get_number(contact, CONTACT_KEY_FLAGS) & ~CONTACT_FLAG_VERIFY;
    qs0 = n & (CONTACT_FLAG_AUDIO | CONTACT_FLAG_UTF) ? ""
                                                      : tr("No communication with %1 is currently possible.").arg(nick);
    if (!m_contact->isMe()) {
      if (n & CONTACT_FLAG_AUDIO) qs0.append(tr("%1 allows you to make audio calls.").arg(nick0)).append("\n");
      if (n & CONTACT_FLAG_UTF) {
        qs0.append(tr("%1 allows you to send text messages.").arg(nick0)).append("\n");
        if (sim_table_get_number(contact, CONTACT_KEY_EDIT)) {
          qs0.append(tr("%1 allows you to edit sent messages.").arg(nick0)).append("\n");
          n |= CONTACT_FLAG_VERIFY;
        }
      }
    } else if (sim_table_get_number(contact, CONTACT_KEY_EDIT)) {
      n |= CONTACT_FLAG_VERIFY;
    }
    qs = n & CONTACT_FLAG_AUDIO
           ? n & CONTACT_FLAG_UTF
               ? tr("Audio calls, Text chat")
               : tr("Audio calls")
           : n & CONTACT_FLAG_UTF
               ? tr("Text chat")
               : tr("none", "communication");
    if (n & CONTACT_FLAG_UTF && n & CONTACT_FLAG_VERIFY) qs.append(tr(", Edit"));
    setInfo(++row, tr("Communication"), qs, qs0.trimmed());

    qs = qs0 = QString();
    if (m_contact->isTest()) {
      if (SimParam::get("ui.chat.showall")) {
        qs.append(tr(", Show All"));
        qs0.append(tr("Add \"Show All\" to chat menu.")).append("\n");
      }
      if (SimParam::get("ui.main.shownostatus")) {
        qs.append(tr(", No Status"));
        qs0.append(tr("Show no status icon for contacts that have unknown status.")).append("\n");
      }
    } else {
      if (n & CONTACT_FLAG_AUDIO_Y) {
        qs.append(tr(", Audio calls"));
        qs0.append(tr("Always allow %1 to make audio calls.").arg(nick)).append("\n");
      }
      if (n & CONTACT_FLAG_AUDIO_N) {
        qs.append(tr(", No audio calls"));
        qs0.append(tr("Never allow %1 to make audio calls.").arg(nick)).append("\n");
      }
      if (n & CONTACT_FLAG_ECHO_Y) {
        qs.append(tr(", Echo"));
        qs0.append(tr("Always cancel echo with %1.").arg(nick)).append("\n");
      }
      if (n & CONTACT_FLAG_ECHO_N) {
        qs.append(tr(", No echo"));
        qs0.append(tr("Never cancel echo with %1.").arg(nick)).append("\n");
      }
      if (n & CONTACT_FLAG_UTF_Y) {
        qs.append(tr(", Text chat"));
        qs0.append(tr("Always allow %1 to send text messages.").arg(nick)).append("\n");
      }
      if (n & CONTACT_FLAG_UTF_N) {
        qs.append(tr(", No text chat"));
        qs0.append(tr("Never allow %1 to send text messages.").arg(nick)).append("\n");
      }
      if (n & CONTACT_FLAG_TYPING_Y) {
        qs.append(tr(", Typing"));
        qs0.append(tr("Always notify %1, as I am typing there.").arg(nick)).append("\n");
      }
      if (n & CONTACT_FLAG_TYPING_N) {
        qs.append(tr(", No typing"));
        qs0.append(tr("Never notify %1, as I am typing there.").arg(nick)).append("\n");
      }
      if (n & CONTACT_FLAG_HISTORY_Y) {
        qs.append(tr(", History"));
        qs0.append(tr("Always keep chat history with %1.").arg(nick)).append("\n");
      }
      if (n & CONTACT_FLAG_HISTORY_N) {
        qs.append(tr(", No history"));
        qs0.append(tr("Never keep chat history with %1.").arg(nick)).append("\n");
      }
      if (n & CONTACT_FLAG_EDIT_Y) {
        qs.append(tr(", Edit"));
        qs0.append(tr("Always allow %1 to edit sent messages.").arg(nick)).append("\n");
      }
      if (n & CONTACT_FLAG_EDIT_N) {
        qs.append(tr(", No edit"));
        qs0.append(tr("Never allow %1 to edit sent messages.").arg(nick)).append("\n");
      }
      if (n & CONTACT_FLAG_SOUND_ON_Y) {
        qs.append(tr(", Logon sound"));
        qs0.append(tr("Always play a sound when %1 logs on.").arg(nick)).append("\n");
      }
      if (n & CONTACT_FLAG_SOUND_ON_N) {
        qs.append(tr(", No logon sound"));
        qs0.append(tr("Never play a sound when %1 logs on.").arg(nick)).append("\n");
      }
      if (n & CONTACT_FLAG_SOUND_OFF_Y) {
        qs.append(tr(", Logoff sound"));
        qs0.append(tr("Always play a sound when %1 logs off.").arg(nick)).append("\n");
      }
      if (n & CONTACT_FLAG_SOUND_OFF_N) {
        qs.append(tr(", No logoff sound"));
        qs0.append(tr("Never play a sound when %1 logs off.").arg(nick)).append("\n");
      }
    }
    if (m_contact->isMe()) qs0 = "";
    setInfo(m_settingsRow = ++row, tr("Settings"), qs.isEmpty() ? "" : qs.mid(2),
            qs0.isNull() ? tr("Using default settings for %1.").arg(nick) : qs0.trimmed());

    if ((qs = m_contact->getAuthText()).isEmpty()) qs = tr("VERIFIED");
    setWindowTitle(tr("Info - %1 (%2)").arg(sim_table_get_pointer(contact, CONTACT_KEY_NICK), qs));
    sim_contact_free_(contact);
  } else {
    ui->infoTable->setRowCount(1);
    setInfo(0, tr("Identifier"), m_contact->getTextId(), tr("%1 does not exist.").arg(nick0));
    setWindowTitle(tr("Info - %1 (%2)").arg(m_contact->m_nick, m_contact->getAuthText()));
  }
  ui->infoTable->resizeColumnsToContents();
  ui->infoTable->resizeRowsToContents();
}

void ContactInfo::onSignalLogout()
{
  m_contact = 0;
  on_okButton_clicked();
}

#define NACT 12
#define NSET 2

void ContactInfo::onCustomContextMenu(const QPoint & pos)
{
  static const char * keys[4] = { CONTACT_KEY_FRAMES_MINE_READ, CONTACT_KEY_FRAMES_MINE_WRITE,
                                  CONTACT_KEY_FRAMES_MINE_ALL, CONTACT_KEY_FRAMES_MINE_LOST };
  static const char * names[NSET] = { QT_TR_NOOP("\"Show All\" in chat menu"),
                                      QT_TR_NOOP("Show no status if not known") };
  static const char * parameters[NSET] = { "ui.chat.showall", "ui.main.shownostatus" };

  QMenu menu(this);
  QModelIndex cell = ui->infoTable->indexAt(pos);
  QAction * c;
  QAction * s;
  QAction * n = 0;
  QAction * r = 0;
  QAction * settings[NSET];
  QAction * actions[NACT];
  int simres = SIM_OK;

  if (cell.isValid() && cell.row() == m_nickRow) {
    n = menu.addAction(tr("&Rename"));
    menu.addSeparator();
  } else if (cell.isValid() && cell.row() == m_statsRow && !m_contact->isTest()) {
    r = menu.addAction(tr("Reset"));
    menu.addSeparator();
  }
  for (int i = 0; i < NACT; i++) actions[i] = 0;
  for (int i = 0; i < NSET; i++) settings[i] = 0;
  if (cell.isValid() && cell.row() == m_settingsRow) {
    if (m_contact->isTest()) {
      for (int i = 0; i < NSET; i++) {
        settings[i] = new QAction(tr(names[i]), &menu);
        settings[i]->setCheckable(true);
        settings[i]->setChecked(SimParam::get(parameters[i]) != 0);
        menu.addAction(settings[i]);
      }
      menu.addSeparator();
    } else {
      for (int i = 0; i < NACT; i++) actions[i] = new QAction(&menu);
      Contacts::get()->addSettingsActions(&menu, m_contact, actions);
    }
  }
  c = menu.addAction(qApp->translate("QWidgetTextControl", "&Copy"));
  menu.addSeparator();
  s = menu.addAction(tr("Select &All"));

  c->setShortcut(Qt::CTRL + Qt::Key_C);
  s->setShortcut(Qt::CTRL + Qt::Key_A);
  if (!cell.isValid()) c->setEnabled(false);
  if (QAction * a = menu.exec(ui->infoTable->viewport()->mapToGlobal(pos))) {
    for (int i = 0; i < NACT; i++) {
      if (actions[i] == a) {
        Contacts::get()->doSettingsAction(m_contact, actions, i);
        onTimerTimeout();
        return;
      }
    }
    if (a == n) {
      Contacts::get()->renameContact(m_contact);
      onTimerTimeout();
    } else if (a == r) {
      for (int i = 0; i < 4; i++) sim_contact_set_(m_contact->m_simId, keys[i], sim_number_new(0));
    } else if (a == c) {
      copySelection();
    } else if (a == s) {
      ui->infoTable->selectAll();
    } else if (m_contact->isTest()) {
      for (int i = 0; i < NSET; i++) {
        if (settings[i] == a) {
          simres = SimParam::set(parameters[i], a->isChecked(), false);
          onTimerTimeout();
          if (i == 1) Contacts::get()->rereadSettings();
          break;
        }
      }
    }
  }

  if (simres != SIM_OK) {
    QString error = tr("Setting for %1 not successful (%2)").arg(m_contact->m_nick).arg(simres);
    qtfix::execMessageBox(true, error, SimCore::getError(simres));
  }
}

void ContactInfo::setToolTip(int row, const QString & toolTip)
{
  ui->infoTable->item(row, 0)->setToolTip(toolTip);
  ui->infoTable->item(row, 1)->setToolTip(toolTip);
  ui->infoTable->item(row, 2)->setToolTip(toolTip);
}

void ContactInfo::setInfo(int row, const QString & name, const QString & value, const QString & toolTip,
                          const QString & pixmapName)
{
  QTableWidgetItem * item = new QTableWidgetItem("        ");
  if (!pixmapName.isEmpty()) {
    QPixmap pixmap;
    pixmap.load(":/" + pixmapName);
    QVariant var(qtfix::fixPixmapSize(pixmap, QFontMetrics(qApp->font("QStatusBar")).ascent(), true));
    item->setData(Qt::DecorationRole, var);
  }
  ui->infoTable->setItem(row, 0, new QTableWidgetItem(name));
  ui->infoTable->setItem(row, 1, item);
  ui->infoTable->setItem(row, 2, new QTableWidgetItem(value));
  setToolTip(row, toolTip.isEmpty() ? toolTip : "<p style='white-space:pre'>" + tr(toolTip.toUtf8().data()));
}

void ContactInfo::setInfo(int row, const QString & name, const QString & value, const QString & toolTip, QColor color)
{
  QTableWidgetItem * item = new QTableWidgetItem(name);
  if (name != tr("Connection") && name != tr("DHT size")) item->setTextColor(color);
  ui->infoTable->setItem(row, 0, item);
  ui->infoTable->setItem(row, 1, new QTableWidgetItem("        "));
  item = new QTableWidgetItem(value);
  item->setTextColor(color);
  ui->infoTable->setItem(row, 2, item);
  setToolTip(row, toolTip.isEmpty() ? toolTip : "<p style='white-space:pre'>" + tr(toolTip.toUtf8().data()));
}

QString ContactInfo::getFlagNames(int flag1, int flag2, const char * name, const char * name1, const char * name2)
{
  QString qs;
  if (flag1) {
    if (flag2) {
      qs.append(tr(", ")).append(name).append(" ").append(name1).append("-").append(name2);
    } else {
      qs.append(tr(", ")).append(name).append(" ").append(name1);
    }
  } else if (flag2) {
    qs.append(tr(", ")).append(name).append(" ").append(name2);
  }
  return qs;
}
