/* This file is part of FSView.
   Copyright (C) 2002, 2003 Josef Weidendorfer <Josef.Weidendorfer@gmx.de>

   KCachegrind is free software; you can redistribute it and/or
   modify it under the terms of the GNU General Public
   License as published by the Free Software Foundation, version 2.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program; see the file COPYING.  If not, write to
   the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
   Boston, MA 02111-1307, USA.
*/

/*
 * FSView specialisaton of TreeMap classes.
 */


#include <qpopupmenu.h>
#include <qdir.h>
#include <qtimer.h>

#include <klocale.h>
#include <kconfig.h>
#include <kglobal.h>
#include <kapplication.h>
#include <kurl.h>
#include <kmimetype.h>
#include <kdebug.h>


#include "fsview.h"


// Inode

Inode::Inode(QString p)
{
  QFileInfo fi(p);
  init(fi);
}

Inode::Inode(QFileInfo& fi)
{
  init(fi);
}

QString Inode::path() const
{
  return _info.filePath();
}

void Inode::setPath(QString p)
{
  QFileInfo fi(p);
  init(fi);

  ((FSView*)widget())->requestUpdate(this);
}

void Inode::init(QFileInfo& fi)
{
  _size = 0.0;
  _sizeEstimation = 0.0;
  _filesEstimation = 0;
  _subDirsUnknown = 0;
  _files = 0;
  _mimeSet = false;
  _mimePixmapSet = false;

  _info = fi;
  if (_info.isDir()) {

    // we always start with estimations >0
    _sizeEstimation++;
    _filesEstimation++;
  }
  else {
    _size = (double) _info.size();
    _files = 0;
  }

  clear();
}

double Inode::size() const
{
    return (_sizeEstimation > _size) ? _sizeEstimation : _size;
}

double Inode::realSize() const
{
    return _size;
}


double Inode::value() const
{
  return size();
}

unsigned int Inode::files() const
{
  if (_filesEstimation > _files)
    return _filesEstimation;
  return _files;
}

// call when real metrics are known
void Inode::setMetrics(double s, unsigned int f)
{
  _sizeEstimation = 0.0;
  _filesEstimation = 0;

  if (s == _size && f == _files) return;

  _size = s;
  _files = f;

  // propagate to parent
  Inode* p = (Inode*) parent();
  if (p) p->gotChildSize(_size, _files);

  // cache metrics if "important"
  int dd = ((FSView*)widget())->pathDepth() + depth();

  // for "/usr" is dd==3
  if ((_files > 500) ||
      ((_files>50) && (dd<5)) ||
      ((_files>1) && (dd<4)))
    FSView::setDirMetric(path(), _size, _files);
}


// this is called after an update(), when the metrics of all
// subdirectories are known
void Inode::finishUpdate()
{
  // add files of this directory
  QDir d(_info.filePath());
  d.setFilter( QDir::Files | QDir::Hidden | QDir::NoSymLinks );
  const QFileInfoList* fiList = d. entryInfoList();

  if (fiList) {
      QFileInfoListIterator it(*fiList);
      QFileInfo* fi;
      while ((fi=it.current())!=0) {
	  ++it;

	  Inode* i = new Inode(*fi);
	  addItem(i);
	  _size += i->realSize();
	  _files++;
      }
  }
  else {
    // for unreadable dirs, use directory size
    _size = _info.size();
  }

  if (0) kdDebug() << "finishUpdate " << path() << ": files " << _files
		   << ", depth " << depth() << endl;

  // after adding all items, switch on sorting
  setSorting(-2, false);

  // the file increment is a kludge to always propagate the metrics
  _files++;
  setMetrics(_size, _files-1);

  ((FSView*)widget())->finished(this);
}

void Inode::update()
{
  if (!_info.isDir()) {
    _info.refresh();
    _size = (double) _info.size();
    return;
  }

  if (0) kdDebug() << "Inode::update(" << path() << ")" << endl;

  // start with estimations...
  if (!FSView::getDirMetric(path(), _sizeEstimation, _filesEstimation)) {
    _sizeEstimation = _size;
    _filesEstimation = _files;
  }
  _size = 0.0;
  _files = 0;
  _subDirsUnknown = 0;

  // first delete all treemap children
  clear();

  // for stable build animation: do in-sort for upper directory
  // (this is not done for deeper dirs because of performance reasons)
  if (depth()>1) setSorting(-1);
  else setSorting(-2,false);

  // request updates for subdirectories
  QDir d(_info.filePath());
  d.setFilter( QDir::Dirs | QDir::Hidden | QDir::NoSymLinks );
  const QFileInfoList* fiList = d. entryInfoList();

  if (fiList) {
      QFileInfoListIterator it(*fiList);
      QFileInfo* fi;
      while ((fi=it.current())!=0) {
	  ++it;

	  if ((fi->fileName() == ".") || (fi->fileName() == "..")) continue;

	  Inode* i = new Inode(*fi);
	  addItem(i);

	  ((FSView*)widget())->requestUpdate(i);
	  _subDirsUnknown++;
	  _files++; // we count directories, too
      }
  }

  if (_subDirsUnknown == 0)  finishUpdate();
  else {
    // our size can have changed (estimation), so parent has to sort if needed.
    if (parent()) parent()->resort(false);
  }
}

void Inode::gotChildSize(double s, unsigned int f)
{
  // if _subDirsUnknown was already 0, this was a user requested
  // update of a subdirectory: we need to update from children...
  if (_subDirsUnknown == 0) {
      double s = 0.0;
      unsigned int f=0;
      TreeMapItemList* list = children();
      TreeMapItem* i;
      for (i=list->first();i;i=list->next()) {
	  s += ((Inode*)i)->realSize();
	  f += ((Inode*)i)->files() + 1;
      }
      setMetrics(s, f);

      // as sizes have changed, the sort order among children can change.
      resort(false);
  }
  else {
    _size += s;
    _files += f;
    _subDirsUnknown--;

    // as sizes have changed, the sort order among children can change.
    resort(false);
    if (_subDirsUnknown == 0) finishUpdate();
  }
}

QColor Inode::backColor() const
{
  QString n;
  switch( ((FSView*)widget())->colorMode() ) {
  case FSView::Depth: 
    {
      int d = ((FSView*)widget())->pathDepth() + depth();
      return QColor((100*d)%360, 192,128, QColor::Hsv);
    }

  case FSView::Name:   n = text(0); break;
  case FSView::Owner:  n = text(4); break;
  case FSView::Group:  n = text(5); break;
  case FSView::Mime:   n = text(6); break;

  default:
    break;
  }

  if (n.isEmpty())
    return widget()->colorGroup().button();
  
  int h = 0, s = 100;
  const char* str = n.ascii();
  while (*str) {
    h = (h * 37 + s* (unsigned)*str) % 256;
    s = (s * 17 + h* (unsigned)*str) % 192;
    str++;
  }
  return QColor(h, 64+s, 192, QColor::Hsv);
}

KMimeType::Ptr Inode::mimeType() const
{
  if (!_mimeSet) {
    KURL u;
    u.setPath(path());
    _mimeType = KMimeType::findByURL( u, 0, true, false );

    _mimeSet = true;
  }
  return _mimeType;
}

QString Inode::text(int i) const
{
  if (i==0) {
    QString name = _info.fileName();
    if (_info.isDir()) name += "/";
    return name;
  }
  if (i==1) {
    QString text;
    double s = size();

    if (s < 1000)
      text = QString("%1 B").arg((int)(s+.5));
    else if (s < 10000)
      text = QString("%1 kB").arg(KGlobal::locale()->formatNumber(s/1000+.005,2));
    else if (s < 100000)
      text = QString("%1 kB").arg(KGlobal::locale()->formatNumber(s/1000+.05,1));
    else if (s < 1000000)
      text = QString("%1 kB").arg((int)(s/1000+.5));
    else if (s < 10000000)
      text = QString("%1 MB").arg(KGlobal::locale()->formatNumber(s/1000000.0+.005,2));
    else if (s < 100000000)
      text = QString("%1 MB").arg(KGlobal::locale()->formatNumber(s/1000000.0+.05,1));
    else if (s < 1000000000)
      text = QString("%1 MB").arg((int)(s/1000000+.5));
    else
      text =  QString("%1 GB").arg(KGlobal::locale()->formatNumber(s/1000000000.0+.005,2));

    if (_sizeEstimation>0) text += "+";
    return text;
  }

  if (i==2) {
    QString text;
    unsigned int f = files();

    if (f>0) {
      while (f>1000) {
        text = QString("%1 %2").arg(QString::number(f).right(3)).arg(text);
        f /= 1000;
      }
      text = QString("%1 %2").arg(QString::number(f)).arg(text);
      if (_filesEstimation>0) text += "+";
    }
    return text;
  }

  if (i==3) return _info.lastModified().toString();
  if (i==4) return _info.owner();
  if (i==5) return _info.group();
  if (i==6) return mimeType()->comment();
  return QString();
}

QPixmap Inode::pixmap(int i) const
{
  if (i!=0) return QPixmap();

  if (!_mimePixmapSet) {
    KURL u;
    u.setPath(path());
    _mimePixmap = mimeType()->pixmap(u, KIcon::Small);

    _mimePixmapSet = true;
  }
  return _mimePixmap;
}



// FSView

QMap<QString, QPair<double, unsigned int> > FSView::_dirMetric;

FSView::FSView(Inode* base, QWidget* parent, const char* name)
  : TreeMapWidget(base, parent, name)
{
  setFieldType(0, i18n("Name"));
  setFieldType(1, i18n("Size"));
  setFieldType(2, i18n("File Count"));
  setFieldType(3, i18n("Last Modified"));
  setFieldType(4, i18n("Owner"));
  setFieldType(5, i18n("Group"));
  setFieldType(6, i18n("Mime Type"));

  // defaults
  setVisibleWidth(4, true);
  setSplitMode(TreeMapItem::Rows);
  setFieldForced(0, true); // show directory names
  setFieldForced(1, true); // show directory sizes
  setSelectionMode(TreeMapWidget::Extended);

  _colorMode = Depth;
  _pathDepth = 0;
  _allowRefresh = true;
  _progressSize = 0;
  _progress = 0;
  _dirsFinished = 0;
  _lastDir = 0;

  // restore TreeMap visualization options of last execution
  KConfigGroup tmconfig(KGlobal::config(), QCString("TreeMap"));
  restoreOptions(&tmconfig);
  QString str = tmconfig.readEntry("ColorMode");
  if (!str.isEmpty()) setColorMode(str);

  if (_dirMetric.count() == 0) {
    // restore metric cache
    KConfigGroup cconfig(KGlobal::config(), QCString("MetricCache"));
    int ccount = cconfig.readNumEntry("Count", 0);
    int i, f;
    double s;
    QString str;
    for (i=1;i<=ccount;i++) {
      str = QString("Dir%1").arg(i);
      if (!cconfig.hasKey(str)) continue;
      str = cconfig.readPathEntry(str);
      s = cconfig.readDoubleNumEntry(QString("Size%1").arg(i), 0.0);
      f = cconfig.readNumEntry(QString("Files%1").arg(i), 0);
      if (s==0.0 || f==0) continue;
      setDirMetric(str, s, f);
    }
  }
}

void FSView::stop()
{
  _needsUpdate.clear();
}

void FSView::setPath(QString p)
{
  Inode* b = (Inode*)base();
  if (!b) return;

  //kdDebug() << "FSView::setPath " << p << endl;

  // stop any previous updating
  stop();

  _path = QFileInfo(p).absFilePath();
  _pathDepth = _path.contains('/');

  b->setPath(_path);
  setCaption(QString("%1 - FSView").arg(_path));
}

KURL::List FSView::selectedUrls()
{
  TreeMapItemList s = selection();
  TreeMapItem* i;
  KURL::List urls;

  for(i=s.first();i;i=s.next()) {
    KURL u;
    u.setPath( ((Inode*)i)->path() );
    urls.append(u);
  }
  return urls;
}

bool FSView::getDirMetric(QString k, double& s, unsigned int& f)
{
  QMap<QString, QPair<double, unsigned int> >::iterator it;

  it = _dirMetric.find(k);
  if (it == _dirMetric.end()) return false;

  s = (*it).first;
  f = (*it).second;

  if (0) kdDebug() << "getDirMetric " << k << endl;
  if (0) kdDebug() << " - got size " << s << ", files " << f << endl;

  return true;
}

void FSView::setDirMetric(QString k, double s, unsigned int f)
{
  if (0) kdDebug() << "setDirMetric " << k << ": size "
		   << s << ", files " << f << endl;
  _dirMetric.insert(k, qMakePair(s, f));
}

void FSView::requestUpdate(Inode* i)
{
  if (0) kdDebug() << "FSView::requestUpdate(" << i->path() << ")" << endl;

  _needsUpdate.append(i);
  if (_needsUpdate.count() == 1) {
    QTimer::singleShot(0, this, SLOT(doUpdate()));
    QTimer::singleShot(100, this, SLOT(doRedraw()));
    _progressSize = 0;
    _progress = 0;
    _dirsFinished = 0;
    _lastDir = 0;
    emit started();
  }

  // for progress info:
  // estimation of "job" size is number of directories with depth <4
  int d = i->depth();
  if (d>1 && d<4) {
    _progressSize--;
    if (0) kdDebug() << "Added for pSize: D " << i->depth()
		     << ", " << i->path() << endl;
  }
  else if (_progressSize<0) _progressSize = -_progressSize;
}

void FSView::finished(Inode* i)
{
  int d = i->depth();
  if (d>1 && d<4) _progress++;
  _lastDir = i;
  _dirsFinished++;
}

void FSView::selected(TreeMapItem* i)
{
  setPath(((Inode*)i)->path());
}

void FSView::contextMenu(TreeMapItem* i, const QPoint& p)
{
  QPopupMenu popup;

  QPopupMenu* spopup = new QPopupMenu();
  QPopupMenu* dpopup = new QPopupMenu();
  QPopupMenu* apopup = new QPopupMenu();
  QPopupMenu* fpopup = new QPopupMenu();

  // choosing from the selection menu will give a selectionChanged() signal
  addSelectionItems(spopup, 901, i);
  popup.insertItem(i18n("Go To"), spopup, 900);

  popup.insertItem(i18n("Go Up"), 2);
  popup.insertSeparator();
  popup.insertItem(i18n("Stop Refresh"), 3);
  popup.setItemEnabled(3, _needsUpdate.count()>0);
  popup.insertItem(i18n("Refresh"), 5);
  popup.setItemEnabled(5, _needsUpdate.count()==0);

  if (i) popup.insertItem(i18n("Refresh '%1'").arg(i->text(0)), 4);
  popup.insertSeparator();
  addDepthStopItems(dpopup, 1001, i);
  popup.insertItem(i18n("Stop at Depth"), dpopup, 1000);
  addAreaStopItems(apopup, 1101, i);
  popup.insertItem(i18n("Stop at Area"), apopup, 1100);
  addFieldStopItems(fpopup, 1201, i);
  popup.insertItem(i18n("Stop at Name"), fpopup, 1200);
  
  popup.insertSeparator();

  QPopupMenu* cpopup = new QPopupMenu();
  addColorItems(cpopup, 1401);
  popup.insertItem(i18n("Color Mode"), cpopup, 1400);
  QPopupMenu* vpopup = new QPopupMenu();
  addVisualizationItems(vpopup, 1301);
  popup.insertItem(i18n("Visualization"), vpopup, 1300);

  _allowRefresh = false;
  int r = popup.exec(mapToGlobal(p));
  _allowRefresh = true;

  if (r==1)
    selected(i);
  else if (r==2) {
    Inode* i = (Inode*) base();
    if (i) setPath(i->path()+"/..");
  }
  else if (r==3)
    stop();
  else if (r==4) {
    ((Inode*)i)->refresh();
    ((Inode*)i)->update();
  }
  else if (r==5) {
    Inode* i = (Inode*) base();
    if (i) i->update();
  }
}

void FSView::saveMetric(KConfigGroup* g)
{
  QMap<QString, QPair<double, unsigned int> >::iterator it;
  int c = 1;
  for (it=_dirMetric.begin();it!=_dirMetric.end();++it) {
    g->writePathEntry(QString("Dir%1").arg(c), it.key());
    g->writeEntry(QString("Size%1").arg(c), (*it).first);
    g->writeEntry(QString("Files%1").arg(c), (*it).second);
    c++;
  }
  g->writeEntry("Count", c-1);
}

void FSView::setColorMode(FSView::ColorMode cm)
{
  if (_colorMode == cm) return;

  _colorMode = cm; 
  redraw();
}

bool FSView::setColorMode(QString mode)
{
  if (mode == "None")       setColorMode(None);
  else if (mode == "Depth") setColorMode(Depth);
  else if (mode == "Name")  setColorMode(Name);
  else if (mode == "Owner") setColorMode(Owner);
  else if (mode == "Group") setColorMode(Group);
  else if (mode == "Mime")  setColorMode(Mime);
  else return false;

  return true;
}

QString FSView::colorModeString() const
{
  QString mode;
  switch(_colorMode) {
  case None:  mode = "None"; break;
  case Depth: mode = "Depth"; break;
  case Name:  mode = "Name"; break;
  case Owner: mode = "Owner"; break;
  case Group: mode = "Group"; break;
  case Mime:  mode = "Mime"; break;
  default:    mode = "Unknown"; break;
  }
  return mode;
}

void FSView::addColorItems(QPopupMenu* popup, int id)
{
  _colorID = id;
  popup->setCheckable(true);

  connect(popup, SIGNAL(activated(int)),
          this, SLOT(colorActivated(int)));

  popup->insertItem(i18n("None"),      id);
  popup->insertItem(i18n("Depth"),     id+1);
  popup->insertItem(i18n("Name"),      id+2);
  popup->insertItem(i18n("Owner"),     id+3);
  popup->insertItem(i18n("Group"),     id+4);
  popup->insertItem(i18n("Mime Type"), id+5);

  switch(colorMode()) {
    case None:  popup->setItemChecked(id,true); break;
    case Depth: popup->setItemChecked(id+1,true); break;
    case Name:  popup->setItemChecked(id+2,true); break;
    case Owner: popup->setItemChecked(id+3,true); break;
    case Group: popup->setItemChecked(id+4,true); break;
    case Mime:  popup->setItemChecked(id+5,true); break;
    default: break;
  }
}

void FSView::colorActivated(int id)
{
  if (id == _colorID)        setColorMode(None);
  else if (id == _colorID+1) setColorMode(Depth);
  else if (id == _colorID+2) setColorMode(Name);
  else if (id == _colorID+3) setColorMode(Owner);
  else if (id == _colorID+4) setColorMode(Group);
  else if (id == _colorID+5) setColorMode(Mime);
}

void FSView::saveFSOptions()
{
  KConfigGroup tmconfig(KGlobal::config(), QCString("TreeMap"));
  saveOptions(&tmconfig);
  tmconfig.writeEntry("ColorMode", colorModeString());

  KConfigGroup gconfig(KGlobal::config(), QCString("General"));
  gconfig.writeEntry("Path", _path);

  KConfigGroup cconfig(KGlobal::config(), QCString("MetricCache"));
  saveMetric(&cconfig);
}

void FSView::quit()
{
  saveFSOptions();
  KApplication::kApplication()->quit();
}

void FSView::doRedraw()
{
  // we update progress every 1/4 second, and redraw every second
  static int redrawCounter = 0;

  bool redo = (_needsUpdate.count()>0);
  if (!redo) redrawCounter = 0;

  if ((_progress>0) && (_progressSize>0) && _lastDir) {
    int percent = _progress * 100 / _progressSize;
    if (0) kdDebug() << "FSView::progress " << percent << " %%, Read "
		     << _dirsFinished << " dirs, in "
		     << _lastDir->path() << endl;
    emit progress(percent, _dirsFinished, _lastDir->path());
  }


  if (_allowRefresh && ((redrawCounter%4)==0)) {
    if (0) kdDebug() << "doRedraw " << _needsUpdate.count() << endl;
    redraw();
  }
  else
    redo = true;

  if (redo) {
    QTimer::singleShot(250, this, SLOT(doRedraw()));
    redrawCounter++;
  }
}


void FSView::doUpdate()
{
  int inodeCounter = 0;
  Inode* i = 0;

  while (inodeCounter < 10) {
    i = _needsUpdate.first();
    if (!i) break;

    if (0) kdDebug() << "doUpdate: " << i->path() << endl;
    i->update();

    _needsUpdate.removeFirst();
    inodeCounter++;
  }

  if (_needsUpdate.count()>0)
    QTimer::singleShot(0, this, SLOT(doUpdate()));
  else
      emit completed(_dirsFinished);
}

#include "fsview.moc"
