/*
 * This file Copyright (C) 2009-2014 Mnemosyne LLC
 *
 * It may be used under the GNU Public License v2 or v3 licenses,
 * or any future license endorsed by Mnemosyne LLC.
 *
 * $Id: details.cc 14238 2014-01-20 23:56:06Z jordan $
 */

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

#include <QCheckBox>
#include <QComboBox>
#include <QDateTime>
#include <QDesktopServices>
#include <QDialogButtonBox>
#include <QDoubleSpinBox>
#include <QEvent>
#include <QFont>
#include <QFontMetrics>
#include <QHBoxLayout>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QInputDialog>
#include <QItemSelectionModel>
#include <QLabel>
#include <QList>
#include <QMap>
#include <QMessageBox>
#include <QPushButton>
#include <QRadioButton>
#include <QResizeEvent>
#include <QSpinBox>
#include <QStringList>
#include <QStyle>
#include <QTabWidget>
#include <QTextBrowser>
#include <QTreeView>
#include <QTreeWidget>
#include <QTreeWidgetItem>
#include <QVBoxLayout>

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

#include "details.h"
#include "file-tree.h"
#include "formatter.h"
#include "hig.h"
#include "prefs.h"
#include "session.h"
#include "squeezelabel.h"
#include "torrent.h"
#include "torrent-model.h"
#include "tracker-delegate.h"
#include "tracker-model.h"
#include "tracker-model-filter.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
  };
}

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

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

  public:

    virtual ~PeerItem () {}

    PeerItem (const Peer& p)
    {
      peer = p;
      int q[4];
      if (sscanf (p.address.toUtf8 ().constData (), "%d.%d.%d.%d", q+0, q+1, q+2, q+3) == 4)
        collatedAddress.sprintf ("%03d.%03d.%03d.%03d", q[0], q[1], q[2], q[3]);
      else
        collatedAddress = p.address;
    }

  public:

    void refresh (const Peer& p) { 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;
      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 collatedAddress < i->collatedAddress;
        }
    }
};

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

QIcon
Details :: 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;
}

Details :: Details (Session       & session,
                    Prefs         & prefs,
                    TorrentModel  & model,
                    QWidget       * parent):
  QDialog (parent, Qt::Dialog),
  mySession (session),
  myPrefs (prefs),
  myModel (model),
  myChangedTorrents (false),
  myHavePendingRefresh (false)
{
  QVBoxLayout * layout = new QVBoxLayout (this);

  setWindowTitle (tr ("Torrent Properties"));

  QTabWidget * t = new QTabWidget (this);
  QWidget * w;
  t->addTab (w = createInfoTab (),      tr ("Information"));
  myWidgets << w;
  t->addTab (w = createPeersTab (),     tr ("Peers"));
  myWidgets << w;
  t->addTab (w = createTrackerTab (),   tr ("Tracker"));
  myWidgets << w;
  t->addTab (w = createFilesTab (),     tr ("Files"));
  myWidgets << w;
  t->addTab (w = createOptionsTab (),   tr ("Options"));
  myWidgets << w;
  layout->addWidget (t);

  QDialogButtonBox * buttons = new QDialogButtonBox (QDialogButtonBox::Close, Qt::Horizontal, this);
  connect (buttons, SIGNAL (rejected ()), this, SLOT (close ()));
  layout->addWidget (buttons);
  QWidget::setAttribute (Qt::WA_DeleteOnClose, true);

  QList<int> initKeys;
  initKeys << Prefs :: SHOW_TRACKER_SCRAPES
           << Prefs :: SHOW_BACKUP_TRACKERS;
  foreach (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);
}

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

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

  myChangedTorrents = true;

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

  myFileTreeView->clear ();
  myIds = ids;
  myTrackerModel->refresh (myModel, myIds);

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

  foreach (QWidget * w, myWidgets)
    w->setEnabled (false);

  onTimer ();
}

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

  switch (key)
    {
      case Prefs :: SHOW_TRACKER_SCRAPES:
        {
          QItemSelectionModel * selectionModel (myTrackerView->selectionModel ());
          const QItemSelection selection (selectionModel->selection ());
          const QModelIndex currentIndex (selectionModel->currentIndex ());
          myTrackerDelegate->setShowMore (myPrefs.getBool (key));
          selectionModel->clear ();
          myTrackerView->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;
    }
}


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

QString
Details :: timeToStringRounded (int seconds)
{
  if (seconds > 60)
    seconds -= (seconds % 60);

  return Formatter::timeToString (seconds);
}

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

void
Details :: getNewData ()
{
  if (!myIds.empty ())
    {
      QSet<int> infos;
      foreach (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
Details :: 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
Details :: 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
  foreach (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 ();
      foreach (const Torrent * 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 ();
    }
  myStateLabel->setText (string);
  const QString stateString = string;

  // myHaveLabel
  double sizeWhenDone = 0;
  double leftUntilDone = 0;
  double available = 0;
  int64_t haveTotal = 0;
  int64_t haveVerified = 0;
  int64_t haveUnverified = 0;
  int64_t verifiedPieces = 0;
  if (torrents.empty ())
    {
      string = none;
    }
  else
    {
      foreach (const Torrent * 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)
        {
          string = tr ("%1 (100%)")
                     .arg (Formatter::sizeToString (haveVerified));
        }
      else if (!haveUnverified)
        {
          string = tr ("%1 of %2 (%3%)")
                     .arg (Formatter::sizeToString (haveVerified))
                     .arg (Formatter::sizeToString (sizeWhenDone))
                     .arg (pct);
        }
      else
        {
          string = tr ("%1 of %2 (%3%), %4 Unverified")
                     .arg (Formatter::sizeToString (haveVerified + haveUnverified))
                     .arg (Formatter::sizeToString (sizeWhenDone))
                     .arg (pct)
                     .arg (Formatter::sizeToString (haveUnverified));
        }
    }
  myHaveLabel->setText (string);

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

  // myDownloadedLabel
  if (torrents.empty ())
    {
      string = none;
    }
  else
    {
      uint64_t d = 0;
      uint64_t f = 0;
      foreach (const Torrent * 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;
    }
  myDownloadedLabel->setText (string);

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

  const QDateTime qdt_now = QDateTime::currentDateTime ();

  // myRunTimeLabel
  if (torrents.empty ())
    {
      string = none;
    }
  else
    {
      bool allPaused = true;
      QDateTime baseline = torrents[0]->lastStarted ();
      foreach (const Torrent * 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));
    }
  myRunTimeLabel->setText (string);


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

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


  // myLastActivityLabel
  if (torrents.empty ())
    {
      string = none;
    }
  else
    {
      QDateTime latest = torrents[0]->lastActivity ();
      foreach (const Torrent * 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));
    }
  myLastActivityLabel->setText (string);


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


  ///
  /// information tab
  ///

  // mySizeLabel
  if (torrents.empty ())
    {
      string = none;
    }
  else
    {
      int pieces = 0;
      uint64_t size = 0;
      uint32_t pieceSize = torrents[0]->pieceSize ();
      foreach (const Torrent * 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));
    }
  mySizeLabel->setText (string);

  // myHashLabel
  string = none;
  if (!torrents.empty ())
    {
      string = torrents[0]->hashString ();
      foreach (const Torrent * t, torrents)
        {
          if (string != t->hashString ())
            {
              string = mixed;
              break;
            }
        }
    }
  myHashLabel->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");
      foreach (const Torrent * t, torrents)
        {
          if (b != t->isPrivate ())
            {
              string = mixed;
              break;
            }
        }
    }
  myPrivacyLabel->setText (string);

  // myCommentBrowser
  string = none;
  if (!torrents.empty ())
    {
      string = torrents[0]->comment ();
      foreach (const Torrent * t, torrents)
        {
          if (string != t->comment ())
            {
              string = mixed;
              break;
            }
        }
    }
  if (myCommentBrowser->toPlainText() != string)
    {
      myCommentBrowser->setText (string);
      myCommentBrowser->setMaximumHeight (QWIDGETSIZE_MAX);
    }

  // 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 ();
      foreach (const Torrent * t, torrents)
        {
          mixed_creator |= (creator != t->creator ());
          mixed_date |= (date != t->dateCreated ().toString ());
        }

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

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


  ///
  ///  Options Tab
  ///

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

      // mySessionLimitCheck
      uniform = true;
      baselineFlag = baseline->honorsSessionLimits ();
      foreach (tor, torrents) if (baselineFlag != tor->honorsSessionLimits ()) { uniform = false; break; }
      mySessionLimitCheck->setChecked (uniform && baselineFlag);

      // mySingleDownCheck
      uniform = true;
      baselineFlag = baseline->downloadIsLimited ();
      foreach (tor, torrents) if (baselineFlag != tor->downloadIsLimited ()) { uniform = false; break; }
      mySingleDownCheck->setChecked (uniform && baselineFlag);

      // mySingleUpCheck
      uniform = true;
      baselineFlag = baseline->uploadIsLimited ();
      foreach (tor, torrents) if (baselineFlag != tor->uploadIsLimited ()) { uniform = false; break; }
      mySingleUpCheck->setChecked (uniform && baselineFlag);

      // myBandwidthPriorityCombo
      uniform = true;
      baselineInt = baseline->getBandwidthPriority ();
      foreach (tor, torrents) if (baselineInt != tor->getBandwidthPriority ()) { uniform = false; break; }
      if (uniform)
        i = myBandwidthPriorityCombo->findData (baselineInt);
      else
        i = -1;
      setIfIdle (myBandwidthPriorityCombo, i);

      setIfIdle (mySingleDownSpin, int (tor->downloadLimit ().KBps ()));
      setIfIdle (mySingleUpSpin, int (tor->uploadLimit ().KBps ()));
      setIfIdle (myPeerLimitSpin, tor->peerLimit ());
    }

  if (!torrents.empty ())
    {
      const Torrent * tor;

      // ratio
      bool uniform = true;
      int baselineInt = torrents[0]->seedRatioMode ();
      foreach (tor, torrents) if (baselineInt != tor->seedRatioMode ()) { uniform = false; break; }

      setIfIdle (myRatioCombo, uniform ? myRatioCombo->findData (baselineInt) : -1);
      myRatioSpin->setVisible (uniform && (baselineInt == TR_RATIOLIMIT_SINGLE));

      setIfIdle (myRatioSpin, tor->seedRatioLimit ());

      // idle
      uniform = true;
      baselineInt = torrents[0]->seedIdleMode ();
      foreach (tor, torrents) if (baselineInt != tor->seedIdleMode ()) { uniform = false; break; }

      setIfIdle (myIdleCombo, uniform ? myIdleCombo->findData (baselineInt) : -1);
      myIdleSpin->setVisible (uniform && (baselineInt == TR_RATIOLIMIT_SINGLE));

      setIfIdle (myIdleSpin, tor->seedIdleLimit ());
    }

  ///
  ///  Tracker tab
  ///

  myTrackerModel->refresh (myModel, myIds);

  ///
  ///  Peers tab
  ///

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

      foreach (const Peer& peer, peers)
        {
          const QString key = idStr + ":" + peer.address;
          PeerItem * item = (PeerItem*) myPeers.value (key, 0);

          if (item == 0) // new peer has connected
            {
              static const QIcon myEncryptionIcon (":/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") : "");
              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;
          foreach (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 ("%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 () ? "" : Formatter::speedToString (peer.rateToPeer));
          item->setText (COL_DOWN, peer.rateToClient.isZero () ? "" : Formatter::speedToString (peer.rateToClient));
          item->setText (COL_PERCENT, peer.progress > 0 ? QString ("%1%").arg ( (int) (peer.progress * 100.0)) : "");
          item->setText (COL_STATUS, code);
          item->setToolTip (COL_STATUS, codeTip);

          peers2.insert (key, item);
        }
    }

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

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

  myChangedTorrents = false;
  myHavePendingRefresh = false;
  foreach (QWidget * w, myWidgets)
    w->setEnabled (true);
}

void
Details :: enableWhenChecked (QCheckBox * box, QWidget * w)
{
  connect (box, SIGNAL (toggled (bool)), w, SLOT (setEnabled (bool)));
  w->setEnabled (box->isChecked ());
}


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

QWidget *
Details :: createInfoTab ()
{
  HIG * hig = new HIG (this);

  hig->addSectionTitle (tr ("Activity"));
  hig->addRow (tr ("Have:"), myHaveLabel = new SqueezeLabel);
  hig->addRow (tr ("Availability:"), myAvailabilityLabel = new SqueezeLabel);
  hig->addRow (tr ("Uploaded:"), myUploadedLabel = new SqueezeLabel);
  hig->addRow (tr ("Downloaded:"), myDownloadedLabel = new SqueezeLabel);
  hig->addRow (tr ("State:"), myStateLabel = new SqueezeLabel);
  hig->addRow (tr ("Running time:"), myRunTimeLabel = new SqueezeLabel);
  hig->addRow (tr ("Remaining time:"), myETALabel = new SqueezeLabel);
  hig->addRow (tr ("Last activity:"), myLastActivityLabel = new SqueezeLabel);
  hig->addRow (tr ("Error:"), myErrorLabel = new SqueezeLabel);
  hig->addSectionDivider ();

  hig->addSectionDivider ();
  hig->addSectionTitle (tr ("Details"));
  hig->addRow (tr ("Size:"), mySizeLabel = new SqueezeLabel);
  hig->addRow (tr ("Location:"), myLocationLabel = new SqueezeLabel);
  hig->addRow (tr ("Hash:"), myHashLabel = new SqueezeLabel);
  hig->addRow (tr ("Privacy:"), myPrivacyLabel = new SqueezeLabel);
  hig->addRow (tr ("Origin:"), myOriginLabel = new SqueezeLabel);
  myOriginLabel->setMinimumWidth (325); // stop long origin strings from resizing the widgit
  hig->addRow (tr ("Comment:"), myCommentBrowser = new QTextBrowser);
  const int h = QFontMetrics (myCommentBrowser->font ()).lineSpacing () * 4;
  myCommentBrowser->setFixedHeight (h);

  hig->finish ();

  return hig;
}

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

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

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

void
Details :: onHonorsSessionLimitsToggled (bool val)
{
  mySession.torrentSet (myIds, TR_KEY_honorsSessionLimits, val);
  getNewData ();
}
void
Details :: onDownloadLimitedToggled (bool val)
{
  mySession.torrentSet (myIds, TR_KEY_downloadLimited, val);
  getNewData ();
}
void
Details :: 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
Details :: onUploadLimitedToggled (bool val)
{
  mySession.torrentSet (myIds, TR_KEY_uploadLimited, val);
  getNewData ();
}

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

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

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

void
Details :: onTrackerSelectionChanged ()
{
  const int selectionCount = myTrackerView->selectionModel ()->selectedRows ().size ();
  myEditTrackerButton->setEnabled (selectionCount == 1);
  myRemoveTrackerButton->setEnabled (selectionCount > 0);
}

void
Details :: 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;

      foreach (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
Details :: onEditTrackerClicked ()
{
  QItemSelectionModel * selectionModel = myTrackerView->selectionModel ();
  QModelIndexList selectedRows = selectionModel->selectedRows ();
  assert (selectedRows.size () == 1);
  QModelIndex i = selectionModel->currentIndex ();
  const TrackerInfo trackerInfo = myTrackerView->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
Details :: onRemoveTrackerClicked ()
{
  // make a map of torrentIds to announce URLs to remove
  QItemSelectionModel * selectionModel = myTrackerView->selectionModel ();
  QModelIndexList selectedRows = selectionModel->selectedRows ();
  QMap<int,int> torrentId_to_trackerIds;
  foreach (QModelIndex i, selectedRows)
    {
      const TrackerInfo inf = myTrackerView->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
  foreach (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 ();
}

QWidget *
Details :: createOptionsTab ()
{
  QSpinBox * s;
  QCheckBox * c;
  QComboBox * m;
  QHBoxLayout * h;
  QDoubleSpinBox * ds;
  const QString speed_K_str = Formatter::unitStr (Formatter::SPEED, Formatter::KB);

  HIG * hig = new HIG (this);
  hig->addSectionTitle (tr ("Speed"));

  c = new QCheckBox (tr ("Honor global &limits"));
  mySessionLimitCheck = c;
  hig->addWideControl (c);
  connect (c, SIGNAL (clicked (bool)), this, SLOT (onHonorsSessionLimitsToggled (bool)));

  c = new QCheckBox (tr ("Limit &download speed (%1):").arg (speed_K_str));
  mySingleDownCheck = c;
  s = new QSpinBox ();
  s->setProperty (PREF_KEY, TR_KEY_downloadLimit);
  s->setSingleStep (5);
  s->setRange (0, INT_MAX);
  mySingleDownSpin = s;
  hig->addRow (c, s);
  enableWhenChecked (c, s);
  connect (c, SIGNAL (clicked (bool)), this, SLOT (onDownloadLimitedToggled (bool)));
  connect (s, SIGNAL (editingFinished ()), this, SLOT (onSpinBoxEditingFinished ()));

  c = new QCheckBox (tr ("Limit &upload speed (%1):").arg (speed_K_str));
  mySingleUpCheck = c;
  s = new QSpinBox ();
  s->setSingleStep (5);
  s->setRange (0, INT_MAX);
  s->setProperty (PREF_KEY, TR_KEY_uploadLimit);
  mySingleUpSpin = s;
  hig->addRow (c, s);
  enableWhenChecked (c, s);
  connect (c, SIGNAL (clicked (bool)), this, SLOT (onUploadLimitedToggled (bool)));
  connect (s, SIGNAL (editingFinished ()), this, SLOT (onSpinBoxEditingFinished ()));

  m = new QComboBox;
  m->addItem (tr ("High"),   TR_PRI_HIGH);
  m->addItem (tr ("Normal"), TR_PRI_NORMAL);
  m->addItem (tr ("Low"),    TR_PRI_LOW);
  connect (m, SIGNAL (currentIndexChanged (int)), this, SLOT (onBandwidthPriorityChanged (int)));
  hig->addRow (tr ("Torrent &priority:"), m);
  myBandwidthPriorityCombo = m;

  hig->addSectionDivider ();
  hig->addSectionTitle (tr ("Seeding Limits"));

  h = new QHBoxLayout ();
  h->setSpacing (HIG :: PAD);
  m = new QComboBox;
  m->addItem (tr ("Use Global Settings"),      TR_RATIOLIMIT_GLOBAL);
  m->addItem (tr ("Seed regardless of ratio"), TR_RATIOLIMIT_UNLIMITED);
  m->addItem (tr ("Stop seeding at ratio:"),   TR_RATIOLIMIT_SINGLE);
  connect (m, SIGNAL (currentIndexChanged (int)), this, SLOT (onRatioModeChanged (int)));
  h->addWidget (myRatioCombo = m);
  ds = new QDoubleSpinBox ();
  ds->setRange (0.5, INT_MAX);
  ds->setProperty (PREF_KEY, TR_KEY_seedRatioLimit);
  connect (ds, SIGNAL (editingFinished ()), this, SLOT (onSpinBoxEditingFinished ()));
  h->addWidget (myRatioSpin = ds);
  hig->addRow (tr ("&Ratio:"), h, m);

  h = new QHBoxLayout ();
  h->setSpacing (HIG :: PAD);
  m = new QComboBox;
  m->addItem (tr ("Use Global Settings"),                 TR_IDLELIMIT_GLOBAL);
  m->addItem (tr ("Seed regardless of activity"),         TR_IDLELIMIT_UNLIMITED);
  m->addItem (tr ("Stop seeding if idle for N minutes:"), TR_IDLELIMIT_SINGLE);
  connect (m, SIGNAL (currentIndexChanged (int)), this, SLOT (onIdleModeChanged (int)));
  h->addWidget (myIdleCombo = m);
  s = new QSpinBox ();
  s->setSingleStep (5);
  s->setRange (1, 9999);
  s->setProperty (PREF_KEY, TR_KEY_seedIdleLimit);
  connect (s, SIGNAL (editingFinished ()), this, SLOT (onSpinBoxEditingFinished ()));
  h->addWidget (myIdleSpin = s);
  hig->addRow (tr ("&Idle:"), h, m);


  hig->addSectionDivider ();
  hig->addSectionTitle (tr ("Peer Connections"));

  s = new QSpinBox ();
  s->setSingleStep (5);
  s->setRange (1, 300);
  s->setProperty (PREF_KEY, TR_KEY_peer_limit);
  connect (s, SIGNAL (editingFinished ()), this, SLOT (onSpinBoxEditingFinished ()));
  myPeerLimitSpin = s;
  hig->addRow (tr ("&Maximum peers:"), s);

  hig->finish ();

  return hig;
}

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

QWidget *
Details :: createTrackerTab ()
{
  QCheckBox * c;
  QPushButton * p;
  QWidget * top = new QWidget;
  QVBoxLayout * v = new QVBoxLayout (top);
  QHBoxLayout * h = new QHBoxLayout ();
  QVBoxLayout * v2 = new QVBoxLayout ();

  v->setSpacing (HIG::PAD_BIG);
  v->setContentsMargins (HIG::PAD_BIG, HIG::PAD_BIG, HIG::PAD_BIG, HIG::PAD_BIG);

  h->setSpacing (HIG::PAD);
  h->setContentsMargins (HIG::PAD_SMALL, HIG::PAD_SMALL, HIG::PAD_SMALL, HIG::PAD_SMALL);

  v2->setSpacing (HIG::PAD);

  myTrackerModel = new TrackerModel;
  myTrackerFilter = new TrackerModelFilter;
  myTrackerFilter->setSourceModel (myTrackerModel);
  myTrackerView = new QTreeView;
  myTrackerView->setModel (myTrackerFilter);
  myTrackerView->setHeaderHidden (true);
  myTrackerView->setSelectionMode (QTreeWidget::ExtendedSelection);
  myTrackerView->setRootIsDecorated (false);
  myTrackerView->setIndentation (2);
  myTrackerView->setItemsExpandable (false);
  myTrackerView->setAlternatingRowColors (true);
  myTrackerView->setItemDelegate (myTrackerDelegate = new TrackerDelegate ());
  connect (myTrackerView->selectionModel (), SIGNAL (selectionChanged (const QItemSelection&, const QItemSelection&)), this, SLOT (onTrackerSelectionChanged ()));
  h->addWidget (myTrackerView, 1);

  p = new QPushButton ();
  p->setIcon (getStockIcon ("list-add", QStyle::SP_DialogOpenButton));
  p->setToolTip (tr ("Add Tracker"));
  myAddTrackerButton = p;
  v2->addWidget (p, 1);
  connect (p, SIGNAL (clicked (bool)), this, SLOT (onAddTrackerClicked ()));

  p = new QPushButton ();
  p->setIcon (getStockIcon ("document-properties", QStyle::SP_DesktopIcon));
  p->setToolTip (tr ("Edit Tracker"));
  myAddTrackerButton = p;
  p->setEnabled (false);
  myEditTrackerButton = p;
  v2->addWidget (p, 1);
  connect (p, SIGNAL (clicked (bool)), this, SLOT (onEditTrackerClicked ()));

  p = new QPushButton ();
  p->setIcon (getStockIcon ("list-remove", QStyle::SP_TrashIcon));
  p->setToolTip (tr ("Remove Trackers"));
  p->setEnabled (false);
  myRemoveTrackerButton = p;
  v2->addWidget (p, 1);
  connect (p, SIGNAL (clicked (bool)), this, SLOT (onRemoveTrackerClicked ()));

  v2->addStretch (1);

  h->addLayout (v2, 1);
  h->setStretch (1, 0);

  v->addLayout (h, 1);

  c = new QCheckBox (tr ("Show &more details"));
  c->setChecked (myPrefs.getBool (Prefs::SHOW_TRACKER_SCRAPES));
  myShowTrackerScrapesCheck = c;
  v->addWidget (c, 1);
  connect (c, SIGNAL (clicked (bool)), this, SLOT (onShowTrackerScrapesToggled (bool)));

  c = new QCheckBox (tr ("Show &backup trackers"));
  c->setChecked (myPrefs.getBool (Prefs::SHOW_BACKUP_TRACKERS));
  myShowBackupTrackersCheck = c;
  v->addWidget (c, 1);
  connect (c, SIGNAL (clicked (bool)), this, SLOT (onShowBackupTrackersToggled (bool)));

  return top;
}

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

QWidget *
Details :: createPeersTab ()
{
  QWidget * top = new QWidget;
  QVBoxLayout * v = new QVBoxLayout (top);
  v->setSpacing (HIG :: PAD_BIG);
  v->setContentsMargins (HIG::PAD_BIG, HIG::PAD_BIG, HIG::PAD_BIG, HIG::PAD_BIG);

  QStringList headers;
  headers << QString () << tr ("Up") << tr ("Down") << tr ("%") << tr ("Status") << tr ("Address") << tr ("Client");
  myPeerTree = new QTreeWidget;
  myPeerTree->setUniformRowHeights (true);
  myPeerTree->setHeaderLabels (headers);
  myPeerTree->setColumnWidth (0, 20);
  myPeerTree->setSortingEnabled (true);
  myPeerTree->sortByColumn (COL_ADDRESS, Qt::AscendingOrder);
  myPeerTree->setRootIsDecorated (false);
  myPeerTree->setTextElideMode (Qt::ElideRight);
  v->addWidget (myPeerTree, 1);

  const QFontMetrics m (font ());
  QSize size = m.size (0, "1024 MiB/s");
  myPeerTree->setColumnWidth (COL_UP, size.width ());
  myPeerTree->setColumnWidth (COL_DOWN, size.width ());
  size = m.size (0, " 100% ");
  myPeerTree->setColumnWidth (COL_PERCENT, size.width ());
  size = m.size (0, "ODUK?EXI");
  myPeerTree->setColumnWidth (COL_STATUS, size.width ());
  size = m.size (0, "888.888.888.888");
  myPeerTree->setColumnWidth (COL_ADDRESS, size.width ());
  size = m.size (0, "Some BitTorrent Client");
  myPeerTree->setColumnWidth (COL_CLIENT, size.width ());
  myPeerTree->setAlternatingRowColors (true);

  return top;
}

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

QWidget *
Details :: createFilesTab ()
{
  myFileTreeView = new FileTreeView ();

  connect (myFileTreeView, SIGNAL (     priorityChanged (const QSet<int>&, int)),
           this,           SLOT ( onFilePriorityChanged (const QSet<int>&, int)));

  connect (myFileTreeView, SIGNAL (     wantedChanged (const QSet<int>&, bool)),
           this,           SLOT ( onFileWantedChanged (const QSet<int>&, bool)));

  connect (myFileTreeView, SIGNAL (pathEdited (const QString&, const QString&)),
           this,           SLOT (onPathEdited (const QString&, const QString&)));

  connect (myFileTreeView, SIGNAL (openRequested (const QString&)),
           this,           SLOT (onOpenRequested (const QString&)));

  return myFileTreeView;
}

void
Details :: 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
Details :: 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
Details :: onPathEdited (const QString& oldpath, const QString& newname)
{
  mySession.torrentRenamePath (myIds, oldpath, newname);
}

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

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

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

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