/*
 * This file Copyright (C) 2009-2015 Mnemosyne LLC
 *
 * It may be used under the GNU GPL versions 2 or 3
 * or any future license endorsed by Mnemosyne LLC.
 *
 * $Id: DetailsDialog.cc 14611 2015-12-06 21:07:37Z jordan $
 */

#include <cassert>
#include <climits> /* INT_MAX */
#include <ctime>

#include <QDateTime>
#include <QDesktopServices>
#include <QEvent>
#include <QFont>
#include <QFontMetrics>
#include <QHeaderView>
#include <QHostAddress>
#include <QInputDialog>
#include <QItemSelectionModel>
#include <QLabel>
#include <QList>
#include <QMap>
#include <QMessageBox>
#include <QResizeEvent>
#include <QStringList>
#include <QStyle>
#include <QTreeWidgetItem>

#include <libtransmission/transmission.h>
#include <libtransmission/utils.h> // tr_getRatio ()

#include "ColumnResizer.h"
#include "DetailsDialog.h"
#include "Formatter.h"
#include "Prefs.h"
#include "Session.h"
#include "SqueezeLabel.h"
#include "Torrent.h"
#include "TorrentModel.h"
#include "TrackerDelegate.h"
#include "TrackerModel.h"
#include "TrackerModelFilter.h"
#include "Utils.h"

class Prefs;
class Session;

/****
*****
****/

namespace
{
  const int REFRESH_INTERVAL_MSEC = 4000;

  const char * PREF_KEY ("pref-key");

  enum // peer columns
  {
    COL_LOCK,
    COL_UP,
    COL_DOWN,
    COL_PERCENT,
    COL_STATUS,
    COL_ADDRESS,
    COL_CLIENT,
    N_COLUMNS
  };

  int
  measureViewItem (QTreeWidget * view, int column, const QString& text)
  {
    const QTreeWidgetItem * headerItem = view->headerItem ();

    const int itemWidth = Utils::measureViewItem (view, text);
    const int headerWidth = Utils::measureHeaderItem (view->header (), headerItem->text (column));

    return std::max (itemWidth, headerWidth);
  }
}

/***
****
***/

class PeerItem: public QTreeWidgetItem
{
    Peer peer;
    mutable QString collatedAddress;
    QString status;

  public:
    PeerItem (const Peer& p): peer(p) {}
    virtual ~PeerItem () {}

  public:
    void refresh (const Peer& p)
    {
      if (p.address != peer.address)
        collatedAddress.clear ();
      peer = p;
    }

    void setStatus (const QString& s) { status = s; }

    virtual bool operator< (const QTreeWidgetItem & other) const
    {
      const PeerItem * i = dynamic_cast<const PeerItem*> (&other);
      QTreeWidget * tw (treeWidget ());
      const int column = tw ? tw->sortColumn () : 0;

      assert (i != nullptr);

      switch (column)
        {
          case COL_UP: return peer.rateToPeer < i->peer.rateToPeer;
          case COL_DOWN: return peer.rateToClient < i->peer.rateToClient;
          case COL_PERCENT: return peer.progress < i->peer.progress;
          case COL_STATUS: return status < i->status;
          case COL_CLIENT: return peer.clientName < i->peer.clientName;
          case COL_LOCK: return peer.isEncrypted && !i->peer.isEncrypted;
          default: return address () < i->address ();
        }
    }

  private:
    const QString& address () const
    {
      if (collatedAddress.isEmpty ())
        {
          QHostAddress ipAddress;
          if (ipAddress.setAddress (peer.address))
            {
              if (ipAddress.protocol () == QAbstractSocket::IPv4Protocol)
                {
                  const quint32 ipv4Address = ipAddress.toIPv4Address ();
                  collatedAddress = QLatin1String ("1-") +
                    QString::fromLatin1 (QByteArray::number (ipv4Address, 16).rightJustified (8, '0'));
                }
              else if (ipAddress.protocol () == QAbstractSocket::IPv6Protocol)
                {
                  const Q_IPV6ADDR ipv6Address = ipAddress.toIPv6Address ();
                  QByteArray tmp (16, '\0');
                  for (int i = 0; i < 16; ++i)
                    tmp[i] = ipv6Address[i];
                  collatedAddress = QLatin1String ("2-") + QString::fromLatin1 (tmp.toHex ());
                }
            }

          if (collatedAddress.isEmpty ())
            collatedAddress = QLatin1String ("3-") + peer.address.toLower ();
        }

      return collatedAddress;
    }
};

/***
****
***/

QIcon
DetailsDialog::getStockIcon (const QString& freedesktop_name, int fallback)
{
  QIcon icon = QIcon::fromTheme (freedesktop_name);

  if (icon.isNull ())
    icon = style ()->standardIcon (QStyle::StandardPixmap (fallback), 0, this);

  return icon;
}

DetailsDialog::DetailsDialog (Session            & session,
                              Prefs              & prefs,
                              const TorrentModel & model,
                              QWidget            * parent):
  BaseDialog (parent),
  mySession (session),
  myPrefs (prefs),
  myModel (model),
  myChangedTorrents (false),
  myHavePendingRefresh (false)
{
  ui.setupUi(this);

  initInfoTab ();
  initPeersTab ();
  initTrackerTab ();
  initFilesTab ();
  initOptionsTab ();

  adjustSize ();
  ui.commentBrowser->setMaximumHeight (QWIDGETSIZE_MAX);

  QList<int> initKeys;
  initKeys << Prefs::SHOW_TRACKER_SCRAPES
           << Prefs::SHOW_BACKUP_TRACKERS;
  for (const int key: initKeys)
    refreshPref (key);

  connect (&myTimer, SIGNAL (timeout ()), this, SLOT (onTimer ()));
  connect (&myPrefs, SIGNAL (changed (int)), this, SLOT (refreshPref (int)));

  onTimer ();
  myTimer.setSingleShot (false);
  myTimer.start (REFRESH_INTERVAL_MSEC);
}

DetailsDialog::~DetailsDialog ()
{
  myTrackerDelegate->deleteLater ();
  myTrackerFilter->deleteLater ();
  myTrackerModel->deleteLater ();
}

void
DetailsDialog::setIds (const QSet<int>& ids)
{
  if (ids == myIds)
    return;

  myChangedTorrents = true;

  // stop listening to the old torrents
  for (const int id: myIds)
    {
      const Torrent * tor = myModel.getTorrentFromId (id);
      if (tor)
        disconnect (tor, SIGNAL (torrentChanged (int)), this, SLOT (onTorrentChanged ()));
    }

  ui.filesView->clear ();
  myIds = ids;
  myTrackerModel->refresh (myModel, myIds);

  // listen to the new torrents
  for (const int id: myIds)
    {
      const Torrent * tor = myModel.getTorrentFromId (id);
      if (tor)
        connect (tor, SIGNAL (torrentChanged (int)), this, SLOT (onTorrentChanged ()));
    }

  for (int i = 0; i < ui.tabs->count (); ++i)
    ui.tabs->widget (i)->setEnabled (false);

  onTimer ();
}

void
DetailsDialog::refreshPref (int key)
{
  QString str;

  switch (key)
    {
      case Prefs::SHOW_TRACKER_SCRAPES:
        {
          QItemSelectionModel * selectionModel (ui.trackersView->selectionModel ());
          const QItemSelection selection (selectionModel->selection ());
          const QModelIndex currentIndex (selectionModel->currentIndex ());
          myTrackerDelegate->setShowMore (myPrefs.getBool (key));
          selectionModel->clear ();
          ui.trackersView->reset ();
          selectionModel->select (selection, QItemSelectionModel::Select);
          selectionModel->setCurrentIndex (currentIndex, QItemSelectionModel::NoUpdate);
          break;
        }

      case Prefs::SHOW_BACKUP_TRACKERS:
        myTrackerFilter->setShowBackupTrackers (myPrefs.getBool (key));
        break;

      default:
        break;
    }
}


/***
****
***/

void
DetailsDialog::onTimer ()
{
  getNewData ();
}

void
DetailsDialog::getNewData ()
{
  if (!myIds.empty ())
    {
      QSet<int> infos;
      for (const int id: myIds)
        {
          const Torrent * tor = myModel.getTorrentFromId (id);
          if (tor->isMagnet ())
            infos.insert (tor->id ());
        }

      if (!infos.isEmpty ())
        mySession.initTorrents (infos);
      mySession.refreshExtraStats (myIds);
    }
}

void
DetailsDialog::onTorrentChanged ()
{
  if (!myHavePendingRefresh)
    {
      myHavePendingRefresh = true;
      QTimer::singleShot (100, this, SLOT (refresh ()));
    }
}

namespace
{
  void setIfIdle (QComboBox * box, int i)
  {
    if (!box->hasFocus ())
      {
        box->blockSignals (true);
        box->setCurrentIndex (i);
        box->blockSignals (false);
      }
  }

  void setIfIdle (QDoubleSpinBox * spin, double value)
  {
    if (!spin->hasFocus ())
      {
        spin->blockSignals (true);
        spin->setValue (value);
        spin->blockSignals (false);
      }
  }

  void setIfIdle (QSpinBox * spin, int value)
  {
    if (!spin->hasFocus ())
      {
        spin->blockSignals (true);
        spin->setValue (value);
        spin->blockSignals (false);
      }
  }
}

void
DetailsDialog::refresh ()
{
  const int n = myIds.size ();
  const bool single = n == 1;
  const QString blank;
  const QFontMetrics fm (fontMetrics ());
  QList<const Torrent*> torrents;
  QString string;
  const QString none = tr ("None");
  const QString mixed = tr ("Mixed");
  const QString unknown = tr ("Unknown");

  // build a list of torrents
  for (const int id: myIds)
    {
      const Torrent * tor = myModel.getTorrentFromId (id);
      if (tor)
        torrents << tor;
    }

  ///
  ///  activity tab
  ///

  // myStateLabel
  if (torrents.empty ())
    {
      string = none;
    }
  else
    {
      bool isMixed = false;
      bool allPaused = true;
      bool allFinished = true;
      const tr_torrent_activity baseline = torrents[0]->getActivity ();
      for (const Torrent * const t: torrents)
        {
          const tr_torrent_activity activity = t->getActivity ();
          if (activity != baseline)
            isMixed = true;
          if (activity != TR_STATUS_STOPPED)
            allPaused = allFinished = false;
          if (!t->isFinished ())
            allFinished = false;
        }

      if (isMixed)
        string = mixed;
      else if (allFinished)
        string = tr ("Finished");
      else if (allPaused)
        string = tr ("Paused");
      else
        string = torrents[0]->activityString ();
    }
  ui.stateValueLabel->setText (string);
  const QString stateString = string;

  // myHaveLabel
  uint64_t sizeWhenDone = 0;
  uint64_t available = 0;
  if (torrents.empty ())
    {
      string = none;
    }
  else
    {
      uint64_t leftUntilDone = 0;
      int64_t haveTotal = 0;
      int64_t haveVerified = 0;
      int64_t haveUnverified = 0;
      int64_t verifiedPieces = 0;

      for (const Torrent * const t: torrents)
        {
          if (t->hasMetadata ())
            {
              haveTotal += t->haveTotal ();
              haveUnverified += t->haveUnverified ();
              const uint64_t v = t->haveVerified ();
              haveVerified += v;
              if (t->pieceSize ())
                verifiedPieces += v / t->pieceSize ();
              sizeWhenDone += t->sizeWhenDone ();
              leftUntilDone += t->leftUntilDone ();
              available += t->sizeWhenDone () - t->leftUntilDone () + t->desiredAvailable ();
            }
        }

      const double d = 100.0 * (sizeWhenDone ? (sizeWhenDone - leftUntilDone) / sizeWhenDone : 1);
      QString pct = Formatter::percentToString (d);

      if (!haveUnverified && !leftUntilDone)
        {
          //: Text following the "Have:" label in torrent properties dialog;
          //: %1 is amount of downloaded and verified data
          string = tr ("%1 (100%)")
                     .arg (Formatter::sizeToString (haveVerified));
        }
      else if (!haveUnverified)
        {
          //: Text following the "Have:" label in torrent properties dialog;
          //: %1 is amount of downloaded and verified data,
          //: %2 is overall size of torrent data,
          //: %3 is percentage (%1/%2*100)
          string = tr ("%1 of %2 (%3%)")
                     .arg (Formatter::sizeToString (haveVerified))
                     .arg (Formatter::sizeToString (sizeWhenDone))
                     .arg (pct);
        }
      else
        {
          //: Text following the "Have:" label in torrent properties dialog;
          //: %1 is amount of downloaded data (both verified and unverified),
          //: %2 is overall size of torrent data,
          //: %3 is percentage (%1/%2*100),
          //: %4 is amount of downloaded but not yet verified data
          string = tr ("%1 of %2 (%3%), %4 Unverified")
                     .arg (Formatter::sizeToString (haveVerified + haveUnverified))
                     .arg (Formatter::sizeToString (sizeWhenDone))
                     .arg (pct)
                     .arg (Formatter::sizeToString (haveUnverified));
        }
    }
  ui.haveValueLabel->setText (string);

  // myAvailabilityLabel
  if (torrents.empty ())
    string = none;
  else if (sizeWhenDone == 0)
    string = none;
  else
    string = QString::fromLatin1 ("%1%").arg (Formatter::percentToString ( (100.0 * available) / sizeWhenDone));
  ui.availabilityValueLabel->setText (string);

  // myDownloadedLabel
  if (torrents.empty ())
    {
      string = none;
    }
  else
    {
      uint64_t d = 0;
      uint64_t f = 0;
      for (const Torrent * const t: torrents)
        {
          d += t->downloadedEver ();
          f += t->failedEver ();
        }
      const QString dstr = Formatter::sizeToString (d);
      const QString fstr = Formatter::sizeToString (f);
      if (f)
        string = tr ("%1 (%2 corrupt)").arg (dstr).arg (fstr);
      else
        string = dstr;
    }
  ui.downloadedValueLabel->setText (string);

  //  myUploadedLabel
  if (torrents.empty ())
    {
      string = none;
    }
  else
    {
      uint64_t u = 0;
      uint64_t d = 0;
      for (const Torrent * const t: torrents)
        {
          u += t->uploadedEver ();
          d += t->downloadedEver ();
        }
      string = tr ("%1 (Ratio: %2)")
                 .arg (Formatter::sizeToString (u))
                 .arg (Formatter::ratioToString (tr_getRatio (u, d)));
    }
  ui.uploadedValueLabel->setText (string);

  const QDateTime qdt_now = QDateTime::currentDateTime ();

  // myRunTimeLabel
  if (torrents.empty ())
    {
      string = none;
    }
  else
    {
      bool allPaused = true;
      QDateTime baseline = torrents[0]->lastStarted ();
      for (const Torrent * const t: torrents)
        {
          if (baseline != t->lastStarted ())
            baseline = QDateTime ();
          if (!t->isPaused ())
            allPaused = false;
        }

      if (allPaused)
        string = stateString; // paused || finished
      else if (baseline.isNull ())
        string = mixed;
      else
        string = Formatter::timeToString (baseline.secsTo (qdt_now));
    }
  ui.runningTimeValueLabel->setText (string);


  // myETALabel
  string.clear ();
  if (torrents.empty ())
    {
      string = none;
    }
  else
    {
      int baseline = torrents[0]->getETA ();
      for (const Torrent * const t: torrents)
        {
          if (baseline != t->getETA ())
            {
              string = mixed;
              break;
            }
        }

      if (string.isEmpty ())
        {
          if (baseline < 0)
            string = tr ("Unknown");
          else
            string = Formatter::timeToString (baseline);
       }
    }
  ui.remainingTimeValueLabel->setText (string);


  // myLastActivityLabel
  if (torrents.empty ())
    {
      string = none;
    }
  else
    {
      QDateTime latest = torrents[0]->lastActivity ();
      for (const Torrent * const t: torrents)
        {
          const QDateTime dt = t->lastActivity ();
          if (latest < dt)
            latest = dt;
        }

      const int seconds = latest.isValid () ? latest.secsTo (qdt_now) : -1;
      if (seconds < 0)
        string = none;
      else if (seconds < 5)
        string = tr ("Active now");
      else
        string = tr ("%1 ago").arg (Formatter::timeToString (seconds));
    }
  ui.lastActivityValueLabel->setText (string);


  if (torrents.empty ())
    {
      string = none;
    }
  else
    {
      string = torrents[0]->getError ();
      for (const Torrent * const t: torrents)
        {
          if (string != t->getError ())
            {
              string = mixed;
              break;
            }
        }
    }
  if (string.isEmpty ())
    string = none;
  ui.errorValueLabel->setText (string);


  ///
  /// information tab
  ///

  // mySizeLabel
  if (torrents.empty ())
    {
      string = none;
    }
  else
    {
      int pieces = 0;
      uint64_t size = 0;
      uint32_t pieceSize = torrents[0]->pieceSize ();
      for (const Torrent * const t: torrents)
        {
          pieces += t->pieceCount ();
          size += t->totalSize ();
          if (pieceSize != t->pieceSize ())
            pieceSize = 0;
        }

      if (!size)
        string = none;
      else if (pieceSize > 0)
        string = tr ("%1 (%Ln pieces @ %2)", "", pieces)
                   .arg (Formatter::sizeToString (size))
                   .arg (Formatter::memToString (pieceSize));
      else
        string = tr ("%1 (%Ln pieces)", "", pieces)
                   .arg (Formatter::sizeToString (size));
    }
  ui.sizeValueLabel->setText (string);

  // myHashLabel
  string = none;
  if (!torrents.empty ())
    {
      string = torrents[0]->hashString ();
      for (const Torrent * const t: torrents)
        {
          if (string != t->hashString ())
            {
              string = mixed;
              break;
            }
        }
    }
  ui.hashValueLabel->setText (string);

  // myPrivacyLabel
  string = none;
  if (!torrents.empty ())
    {
      bool b = torrents[0]->isPrivate ();
      string = b ? tr ("Private to this tracker -- DHT and PEX disabled")
                 : tr ("Public torrent");
      for (const Torrent * const t: torrents)
        {
          if (b != t->isPrivate ())
            {
              string = mixed;
              break;
            }
        }
    }
  ui.privacyValueLabel->setText (string);

  // myCommentBrowser
  string = none;
  bool isCommentMixed = false;
  if (!torrents.empty ())
    {
      string = torrents[0]->comment ();
      for (const Torrent * const t: torrents)
        {
          if (string != t->comment ())
            {
              string = mixed;
              isCommentMixed = true;
              break;
            }
        }
    }
  if (ui.commentBrowser->toPlainText() != string)
    {
      ui.commentBrowser->setText (string);
    }
  ui.commentBrowser->setEnabled (!isCommentMixed && !string.isEmpty ());

  // myOriginLabel
  string = none;
  if (!torrents.empty ())
    {
      bool mixed_creator=false, mixed_date=false;
      const QString creator = torrents[0]->creator ();
      const QString date = torrents[0]->dateCreated ().toString ();
      for (const Torrent * const t: torrents)
        {
          mixed_creator |= (creator != t->creator ());
          mixed_date |= (date != t->dateCreated ().toString ());
        }

      const bool empty_creator = creator.isEmpty ();
      const bool empty_date = date.isEmpty ();

      if (mixed_creator || mixed_date)
        string = mixed;
      else if (empty_creator && empty_date)
        string = tr ("N/A");
      else if (empty_date && !empty_creator)
        string = tr ("Created by %1").arg (creator);
      else if (empty_creator && !empty_date)
        string = tr ("Created on %1").arg (date);
      else
        string = tr ("Created by %1 on %2").arg (creator).arg (date);
    }
  ui.originValueLabel->setText (string);

  // myLocationLabel
  string = none;
  if (!torrents.empty ())
    {
      string = torrents[0]->getPath ();
      for (const Torrent * const t: torrents)
        {
          if (string != t->getPath ())
            {
              string = mixed;
              break;
            }
        }
    }
  ui.locationValueLabel->setText (string);


  ///
  ///  Options Tab
  ///

  if (myChangedTorrents && !torrents.empty ())
    {
      int i;
      bool uniform;
      bool baselineFlag;
      int baselineInt;
      const Torrent& baseline = *torrents.front ();

      // mySessionLimitCheck
      uniform = true;
      baselineFlag = baseline.honorsSessionLimits ();
      for (const Torrent * const tor: torrents) if (baselineFlag != tor->honorsSessionLimits ()) { uniform = false; break; }
      ui.sessionLimitCheck->setChecked (uniform && baselineFlag);

      // mySingleDownCheck
      uniform = true;
      baselineFlag = baseline.downloadIsLimited ();
      for (const Torrent * const tor: torrents) if (baselineFlag != tor->downloadIsLimited ()) { uniform = false; break; }
      ui.singleDownCheck->setChecked (uniform && baselineFlag);

      // mySingleUpCheck
      uniform = true;
      baselineFlag = baseline.uploadIsLimited ();
      for (const Torrent * const tor: torrents) if (baselineFlag != tor->uploadIsLimited ()) { uniform = false; break; }
      ui.singleUpCheck->setChecked (uniform && baselineFlag);

      // myBandwidthPriorityCombo
      uniform = true;
      baselineInt = baseline.getBandwidthPriority ();
      for (const Torrent * const tor: torrents) if (baselineInt != tor->getBandwidthPriority ()) { uniform = false; break; }
      if (uniform)
        i = ui.bandwidthPriorityCombo->findData (baselineInt);
      else
        i = -1;
      setIfIdle (ui.bandwidthPriorityCombo, i);

      setIfIdle (ui.singleDownSpin, int (baseline.downloadLimit ().KBps ()));
      setIfIdle (ui.singleUpSpin, int (baseline.uploadLimit ().KBps ()));
      setIfIdle (ui.peerLimitSpin, baseline.peerLimit ());
    }

  if (!torrents.empty ())
    {
      const Torrent& baseline = *torrents.front ();

      // ratio
      bool uniform = true;
      int baselineInt = baseline.seedRatioMode ();
      for (const Torrent * const tor: torrents) if (baselineInt != tor->seedRatioMode ()) { uniform = false; break; }

      setIfIdle (ui.ratioCombo, uniform ? ui.ratioCombo->findData (baselineInt) : -1);
      ui.ratioSpin->setVisible (uniform && (baselineInt == TR_RATIOLIMIT_SINGLE));

      setIfIdle (ui.ratioSpin, baseline.seedRatioLimit ());

      // idle
      uniform = true;
      baselineInt = baseline.seedIdleMode ();
      for (const Torrent * const tor: torrents) if (baselineInt != tor->seedIdleMode ()) { uniform = false; break; }

      setIfIdle (ui.idleCombo, uniform ? ui.idleCombo->findData (baselineInt) : -1);
      ui.idleSpin->setVisible (uniform && (baselineInt == TR_RATIOLIMIT_SINGLE));

      setIfIdle (ui.idleSpin, baseline.seedIdleLimit ());
      onIdleLimitChanged ();
    }

  ///
  ///  Tracker tab
  ///

  myTrackerModel->refresh (myModel, myIds);

  ///
  ///  Peers tab
  ///

  QMap<QString,QTreeWidgetItem*> peers2;
  QList<QTreeWidgetItem*> newItems;
  for (const Torrent * const t: torrents)
    {
      const QString idStr (QString::number (t->id ()));
      PeerList peers = t->peers ();

      for (const Peer& peer: peers)
        {
          const QString key = idStr + QLatin1Char (':') + peer.address;
          PeerItem * item = static_cast<PeerItem*> (myPeers.value (key, 0));

          if (item == 0) // new peer has connected
            {
              static const QIcon myEncryptionIcon (QLatin1String (":/icons/encrypted.png"));
              static const QIcon myEmptyIcon;
              item = new PeerItem (peer);
              item->setTextAlignment (COL_UP, Qt::AlignRight|Qt::AlignVCenter);
              item->setTextAlignment (COL_DOWN, Qt::AlignRight|Qt::AlignVCenter);
              item->setTextAlignment (COL_PERCENT, Qt::AlignRight|Qt::AlignVCenter);
              item->setIcon (COL_LOCK, peer.isEncrypted ? myEncryptionIcon : myEmptyIcon);
              item->setToolTip (COL_LOCK, peer.isEncrypted ? tr ("Encrypted connection") : QString ());
              item->setText (COL_ADDRESS, peer.address);
              item->setText (COL_CLIENT, peer.clientName);
              newItems << item;
            }

          const QString code = peer.flagStr;
          item->setStatus (code);
          item->refresh (peer);

          QString codeTip;
          for (const QChar ch: code)
            {
              QString txt;
              switch (ch.unicode ())
                {
                  case 'O': txt = tr ("Optimistic unchoke"); break;
                  case 'D': txt = tr ("Downloading from this peer"); break;
                  case 'd': txt = tr ("We would download from this peer if they would let us"); break;
                  case 'U': txt = tr ("Uploading to peer"); break;
                  case 'u': txt = tr ("We would upload to this peer if they asked"); break;
                  case 'K': txt = tr ("Peer has unchoked us, but we're not interested"); break;
                  case '?': txt = tr ("We unchoked this peer, but they're not interested"); break;
                  case 'E': txt = tr ("Encrypted connection"); break;
                  case 'H': txt = tr ("Peer was discovered through DHT"); break;
                  case 'X': txt = tr ("Peer was discovered through Peer Exchange (PEX)"); break;
                  case 'I': txt = tr ("Peer is an incoming connection"); break;
                  case 'T': txt = tr ("Peer is connected over uTP"); break;
                }

              if (!txt.isEmpty ())
                codeTip += QString::fromLatin1 ("%1: %2\n").arg (ch).arg (txt);
            }

          if (!codeTip.isEmpty ())
            codeTip.resize (codeTip.size ()-1); // eat the trailing linefeed

          item->setText (COL_UP, peer.rateToPeer.isZero () ? QString () : Formatter::speedToString (peer.rateToPeer));
          item->setText (COL_DOWN, peer.rateToClient.isZero () ? QString () : Formatter::speedToString (peer.rateToClient));
          item->setText (COL_PERCENT, peer.progress > 0 ? QString::fromLatin1 ("%1%").arg(int(peer.progress * 100.0)) : QString ());
          item->setText (COL_STATUS, code);
          item->setToolTip (COL_STATUS, codeTip);

          peers2.insert (key, item);
        }
    }

  ui.peersView->addTopLevelItems (newItems);
  for (const QString& key: myPeers.keys ())
    {
      if (!peers2.contains (key)) // old peer has disconnected
        {
          QTreeWidgetItem * item = myPeers.value (key, 0);
          ui.peersView->takeTopLevelItem (ui.peersView->indexOfTopLevelItem (item));
          delete item;
        }
    }
  myPeers = peers2;

  if (!single)
    ui.filesView->clear ();
  if (single)
    ui.filesView->update (torrents[0]->files (), myChangedTorrents);

  myChangedTorrents = false;
  myHavePendingRefresh = false;
  for (int i = 0; i < ui.tabs->count (); ++i)
    ui.tabs->widget (i)->setEnabled (true);
}

/***
****
***/

void
DetailsDialog::initInfoTab ()
{
  const int h = QFontMetrics (ui.commentBrowser->font ()).lineSpacing () * 4;
  ui.commentBrowser->setFixedHeight (h);

  ColumnResizer * cr (new ColumnResizer (this));
  cr->addLayout (ui.activitySectionLayout);
  cr->addLayout (ui.detailsSectionLayout);
  cr->update ();
}

/***
****
***/

void
DetailsDialog::onShowTrackerScrapesToggled (bool val)
{
  myPrefs.set (Prefs::SHOW_TRACKER_SCRAPES, val);
}

void
DetailsDialog::onShowBackupTrackersToggled (bool val)
{
  myPrefs.set (Prefs::SHOW_BACKUP_TRACKERS, val);
}

void
DetailsDialog::onHonorsSessionLimitsToggled (bool val)
{
  mySession.torrentSet (myIds, TR_KEY_honorsSessionLimits, val);
  getNewData ();
}
void
DetailsDialog::onDownloadLimitedToggled (bool val)
{
  mySession.torrentSet (myIds, TR_KEY_downloadLimited, val);
  getNewData ();
}
void
DetailsDialog::onSpinBoxEditingFinished ()
{
  const QObject * spin = sender ();
  const tr_quark key = spin->property (PREF_KEY).toInt ();
  const QDoubleSpinBox * d = qobject_cast<const QDoubleSpinBox*> (spin);
  if (d)
    mySession.torrentSet (myIds, key, d->value ());
  else
    mySession.torrentSet (myIds, key, qobject_cast<const QSpinBox*> (spin)->value ());
  getNewData ();
}

void
DetailsDialog::onUploadLimitedToggled (bool val)
{
  mySession.torrentSet (myIds, TR_KEY_uploadLimited, val);
  getNewData ();
}

void
DetailsDialog::onIdleModeChanged (int index)
{
  const int val = ui.idleCombo->itemData (index).toInt ();
  mySession.torrentSet (myIds, TR_KEY_seedIdleMode, val);
  getNewData ();
}

void
DetailsDialog::onIdleLimitChanged ()
{
  //: Spin box suffix, "Stop seeding if idle for: [ 5 minutes ]" (includes leading space after the number, if needed)
  const QString unitsSuffix = tr (" minute(s)", 0, ui.idleSpin->value ());
  if (ui.idleSpin->suffix () != unitsSuffix)
    ui.idleSpin->setSuffix (unitsSuffix);
}

void
DetailsDialog::onRatioModeChanged (int index)
{
  const int val = ui.ratioCombo->itemData (index).toInt ();
  mySession.torrentSet (myIds, TR_KEY_seedRatioMode, val);
}

void
DetailsDialog::onBandwidthPriorityChanged (int index)
{
  if (index != -1)
    {
      const int priority = ui.bandwidthPriorityCombo->itemData (index).toInt ();
      mySession.torrentSet (myIds, TR_KEY_bandwidthPriority, priority);
      getNewData ();
    }
}

void
DetailsDialog::onTrackerSelectionChanged ()
{
  const int selectionCount = ui.trackersView->selectionModel ()->selectedRows ().size ();
  ui.editTrackerButton->setEnabled (selectionCount == 1);
  ui.removeTrackerButton->setEnabled (selectionCount > 0);
}

void
DetailsDialog::onAddTrackerClicked ()
{
  bool ok = false;
  const QString url = QInputDialog::getText (this,
                                             tr ("Add URL "),
                                             tr ("Add tracker announce URL:"),
                                             QLineEdit::Normal, QString (), &ok);
  if (!ok)
    {
      // user pressed "cancel" -- noop
    }
  else if (!QUrl (url).isValid ())
    {
      QMessageBox::warning (this, tr ("Error"), tr ("Invalid URL \"%1\"").arg (url));
    }
  else
    {
      QSet<int> ids;

      for (const int id: myIds)
        if (myTrackerModel->find (id,url) == -1)
          ids.insert (id);

      if (ids.empty ()) // all the torrents already have this tracker
        {
          QMessageBox::warning (this, tr ("Error"), tr ("Tracker already exists."));
        }
        else
        {
          QStringList urls;
          urls << url;
          mySession.torrentSet (ids, TR_KEY_trackerAdd, urls);
          getNewData ();
        }
    }
}

void
DetailsDialog::onEditTrackerClicked ()
{
  QItemSelectionModel * selectionModel = ui.trackersView->selectionModel ();
  QModelIndexList selectedRows = selectionModel->selectedRows ();
  assert (selectedRows.size () == 1);
  QModelIndex i = selectionModel->currentIndex ();
  const TrackerInfo trackerInfo = ui.trackersView->model ()->data (i, TrackerModel::TrackerRole).value<TrackerInfo> ();

  bool ok = false;
  const QString newval = QInputDialog::getText (this,
                                                tr ("Edit URL "),
                                                tr ("Edit tracker announce URL:"),
                                                QLineEdit::Normal,
                                                trackerInfo.st.announce, &ok);

  if (!ok)
    {
      // user pressed "cancel" -- noop
    }
  else if (!QUrl (newval).isValid ())
    {
      QMessageBox::warning (this, tr ("Error"), tr ("Invalid URL \"%1\"").arg (newval));
    }
    else
    {
      QSet<int> ids;
      ids << trackerInfo.torrentId;

      const QPair<int,QString> idUrl = qMakePair (trackerInfo.st.id, newval);

      mySession.torrentSet (ids, TR_KEY_trackerReplace, idUrl);
      getNewData ();
    }
}

void
DetailsDialog::onRemoveTrackerClicked ()
{
  // make a map of torrentIds to announce URLs to remove
  QItemSelectionModel * selectionModel = ui.trackersView->selectionModel ();
  QModelIndexList selectedRows = selectionModel->selectedRows ();
  QMap<int,int> torrentId_to_trackerIds;
  for (const QModelIndex& i: selectedRows)
    {
      const TrackerInfo inf = ui.trackersView->model ()->data (i, TrackerModel::TrackerRole).value<TrackerInfo> ();
      torrentId_to_trackerIds.insertMulti (inf.torrentId, inf.st.id);
    }

  // batch all of a tracker's torrents into one command
  for (const int id: torrentId_to_trackerIds.uniqueKeys ())
    {
      QSet<int> ids;
      ids << id;
      mySession.torrentSet (ids, TR_KEY_trackerRemove, torrentId_to_trackerIds.values (id));
    }

  selectionModel->clearSelection ();
  getNewData ();
}

void
DetailsDialog::initOptionsTab ()
{
  const QString speed_K_str = Formatter::unitStr (Formatter::SPEED, Formatter::KB);

  ui.singleDownSpin->setSuffix (QString::fromLatin1 (" %1").arg (speed_K_str));
  ui.singleUpSpin->setSuffix (QString::fromLatin1 (" %1").arg (speed_K_str));

  ui.singleDownSpin->setProperty (PREF_KEY, TR_KEY_downloadLimit);
  ui.singleUpSpin->setProperty (PREF_KEY, TR_KEY_uploadLimit);
  ui.ratioSpin->setProperty (PREF_KEY, TR_KEY_seedRatioLimit);
  ui.idleSpin->setProperty (PREF_KEY, TR_KEY_seedIdleLimit);
  ui.peerLimitSpin->setProperty (PREF_KEY, TR_KEY_peer_limit);

  ui.bandwidthPriorityCombo->addItem (tr ("High"),   TR_PRI_HIGH);
  ui.bandwidthPriorityCombo->addItem (tr ("Normal"), TR_PRI_NORMAL);
  ui.bandwidthPriorityCombo->addItem (tr ("Low"),    TR_PRI_LOW);

  ui.ratioCombo->addItem (tr ("Use Global Settings"),      TR_RATIOLIMIT_GLOBAL);
  ui.ratioCombo->addItem (tr ("Seed regardless of ratio"), TR_RATIOLIMIT_UNLIMITED);
  ui.ratioCombo->addItem (tr ("Stop seeding at ratio:"),   TR_RATIOLIMIT_SINGLE);

  ui.idleCombo->addItem (tr ("Use Global Settings"),         TR_IDLELIMIT_GLOBAL);
  ui.idleCombo->addItem (tr ("Seed regardless of activity"), TR_IDLELIMIT_UNLIMITED);
  ui.idleCombo->addItem (tr ("Stop seeding if idle for:"),   TR_IDLELIMIT_SINGLE);

  ColumnResizer * cr (new ColumnResizer (this));
  cr->addLayout (ui.speedSectionLayout);
  cr->addLayout (ui.seedingLimitsSectionRatioLayout);
  cr->addLayout (ui.seedingLimitsSectionIdleLayout);
  cr->addLayout (ui.peerConnectionsSectionLayout);
  cr->update ();

  connect (ui.sessionLimitCheck, SIGNAL (clicked (bool)), SLOT (onHonorsSessionLimitsToggled (bool)));
  connect (ui.singleDownCheck, SIGNAL (clicked (bool)), SLOT (onDownloadLimitedToggled (bool)));
  connect (ui.singleDownSpin, SIGNAL (editingFinished ()), SLOT (onSpinBoxEditingFinished ()));
  connect (ui.singleUpCheck, SIGNAL (clicked (bool)), SLOT (onUploadLimitedToggled (bool)));
  connect (ui.singleUpSpin, SIGNAL (editingFinished ()), SLOT (onSpinBoxEditingFinished ()));
  connect (ui.bandwidthPriorityCombo, SIGNAL (currentIndexChanged (int)), SLOT (onBandwidthPriorityChanged (int)));
  connect (ui.ratioCombo, SIGNAL (currentIndexChanged (int)), SLOT (onRatioModeChanged (int)));
  connect (ui.ratioSpin, SIGNAL (editingFinished ()), SLOT (onSpinBoxEditingFinished ()));
  connect (ui.idleCombo, SIGNAL (currentIndexChanged (int)), SLOT (onIdleModeChanged (int)));
  connect (ui.idleSpin, SIGNAL (editingFinished ()), SLOT (onSpinBoxEditingFinished ()));
  connect (ui.idleSpin, SIGNAL (valueChanged (int)), SLOT (onIdleLimitChanged ()));
  connect (ui.peerLimitSpin, SIGNAL (editingFinished ()), SLOT (onSpinBoxEditingFinished ()));
}

/***
****
***/

void
DetailsDialog::initTrackerTab ()
{
  myTrackerModel = new TrackerModel ();
  myTrackerFilter = new TrackerModelFilter ();
  myTrackerFilter->setSourceModel (myTrackerModel);
  myTrackerDelegate = new TrackerDelegate ();

  ui.trackersView->setModel (myTrackerFilter);
  ui.trackersView->setItemDelegate (myTrackerDelegate);

  ui.addTrackerButton->setIcon (getStockIcon (QLatin1String ("list-add"), QStyle::SP_DialogOpenButton));
  ui.editTrackerButton->setIcon (getStockIcon (QLatin1String ("document-properties"), QStyle::SP_DesktopIcon));
  ui.removeTrackerButton->setIcon (getStockIcon (QLatin1String ("list-remove"), QStyle::SP_TrashIcon));

  ui.showTrackerScrapesCheck->setChecked (myPrefs.getBool (Prefs::SHOW_TRACKER_SCRAPES));
  ui.showBackupTrackersCheck->setChecked (myPrefs.getBool (Prefs::SHOW_BACKUP_TRACKERS));

  connect (ui.trackersView->selectionModel (), SIGNAL (selectionChanged (QItemSelection, QItemSelection)),
    SLOT (onTrackerSelectionChanged ()));
  connect (ui.addTrackerButton, SIGNAL (clicked ()), SLOT (onAddTrackerClicked ()));
  connect (ui.editTrackerButton, SIGNAL (clicked ()), SLOT (onEditTrackerClicked ()));
  connect (ui.removeTrackerButton, SIGNAL (clicked ()), SLOT (onRemoveTrackerClicked ()));
  connect (ui.showTrackerScrapesCheck, SIGNAL (clicked (bool)), SLOT (onShowTrackerScrapesToggled (bool)));
  connect (ui.showBackupTrackersCheck, SIGNAL (clicked (bool)), SLOT (onShowBackupTrackersToggled (bool)));

  onTrackerSelectionChanged ();
}

/***
****
***/

void
DetailsDialog::initPeersTab ()
{
  QStringList headers;
  headers << QString () << tr ("Up") << tr ("Down") << tr ("%") << tr ("Status") << tr ("Address") << tr ("Client");

  ui.peersView->setHeaderLabels (headers);
  ui.peersView->sortByColumn (COL_ADDRESS, Qt::AscendingOrder);

  ui.peersView->setColumnWidth (COL_LOCK, 20);
  ui.peersView->setColumnWidth (COL_UP, measureViewItem (ui.peersView, COL_UP, QLatin1String ("1024 MiB/s")));
  ui.peersView->setColumnWidth (COL_DOWN, measureViewItem (ui.peersView, COL_DOWN, QLatin1String ("1024 MiB/s")));
  ui.peersView->setColumnWidth (COL_PERCENT, measureViewItem (ui.peersView, COL_PERCENT, QLatin1String ("100%")));
  ui.peersView->setColumnWidth (COL_STATUS, measureViewItem (ui.peersView, COL_STATUS, QLatin1String ("ODUK?EXI")));
  ui.peersView->setColumnWidth (COL_ADDRESS, measureViewItem (ui.peersView, COL_ADDRESS, QLatin1String ("888.888.888.888")));
}

/***
****
***/

void
DetailsDialog::initFilesTab ()
{
  connect (ui.filesView, SIGNAL (priorityChanged (QSet<int>, int)), SLOT (onFilePriorityChanged (QSet<int>, int)));
  connect (ui.filesView, SIGNAL (wantedChanged (QSet<int>, bool)), SLOT (onFileWantedChanged (QSet<int>, bool)));
  connect (ui.filesView, SIGNAL (pathEdited (QString, QString)), SLOT (onPathEdited (QString, QString)));
  connect (ui.filesView, SIGNAL (openRequested (QString)), SLOT (onOpenRequested (QString)));
}

void
DetailsDialog::onFilePriorityChanged (const QSet<int>& indices, int priority)
{
  tr_quark key;

  switch (priority)
    {
      case TR_PRI_LOW:
        key = TR_KEY_priority_low;
        break;

      case TR_PRI_HIGH:
        key = TR_KEY_priority_high;
        break;

      default:
        key = TR_KEY_priority_normal;
        break;
    }

  mySession.torrentSet (myIds, key, indices.toList ());
  getNewData ();
}

void
DetailsDialog::onFileWantedChanged (const QSet<int>& indices, bool wanted)
{
  const tr_quark key = wanted ? TR_KEY_files_wanted : TR_KEY_files_unwanted;
  mySession.torrentSet (myIds, key, indices.toList ());
  getNewData ();
}

void
DetailsDialog::onPathEdited (const QString& oldpath, const QString& newname)
{
  mySession.torrentRenamePath (myIds, oldpath, newname);
}

void
DetailsDialog::onOpenRequested (const QString& path)
{
  if (!mySession.isLocal ())
    return;

  for (const int id: myIds)
    {
      const Torrent * const tor = myModel.getTorrentFromId (id);
      if (tor == NULL)
        continue;

      const QString localFilePath = tor->getPath () + QLatin1Char ('/') + path;
      if (!QFile::exists (localFilePath))
        continue;

      if (QDesktopServices::openUrl (QUrl::fromLocalFile (localFilePath)))
        break;
    }
}
