/* 
 *  Copyright (C) 2004 Girish Ramakrishnan All Rights Reserved.
 *	
 * This 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; either version 2 of the License, or
 * (at your option) any later version.
 * 
 * This software 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 software; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307,
 * USA.
 */

// $Id: traylabelmgr.cpp,v 1.10 2005/02/09 03:38:43 cs19713 Exp $

#include <qdir.h>
#include <qapplication.h>
#include <qmessagebox.h>
#include <qtimer.h>
#include <qfile.h>
#include <qaction.h>
#include <qpopupmenu.h>
#include <qtextstream.h>
#include <qfiledialog.h>

#include "trace.h"
#include "traylabelmgr.h"
#include "util.h"

#include <Xmu/WinUtil.h>
#include <errno.h>
#include <stdlib.h>

TrayLabelMgr* TrayLabelMgr::gTrayLabelMgr = NULL;
const char *TrayLabelMgr::mOptionString = "+abdefi:lmop:qtw:";

TrayLabelMgr* TrayLabelMgr::instance(void)
{
  if (gTrayLabelMgr) return gTrayLabelMgr;
  TRACE("Creating new instance");
  return (gTrayLabelMgr = new TrayLabelMgr());
}

TrayLabelMgr::TrayLabelMgr() : mReady(false), mHiddenLabelsCount(0)
{
  // Set ourselves up to be called from the application loop
  QTimer::singleShot(0, this, SLOT(startup()));
}

TrayLabelMgr::~TrayLabelMgr()
{
  undockAll();
}

void TrayLabelMgr::about(void)
{
  if (QMessageBox::information(NULL, tr("About KDocker"),
        tr("Bugs/wishes to Girish Ramakrishnan (gramakri@uiuc.edu)\n"
           "English translation by Girish (gramakri@uiuc.edu)\n\n"
           "http://kdocker.sourceforge.net for updates"),
        QString::null, SHOW_TRACE_TEXT) == 1) SHOW_TRACE();
}

void TrayLabelMgr::startup(void)
{
  const int WAIT_TIME = 10;
  static int wait_time = WAIT_TIME;
  /*
   * If it appears that we were launched from some startup script (check whether
   * stdout is a tty) OR if we are getting restored, wait for WAIT_TIME until
   * the system tray shows up (before informing the user)
   */
  static bool do_wait = !isatty(fileno(stdout)) || qApp->isSessionRestored();

  SysTrayState state = sysTrayStatus(QPaintDevice::x11AppDisplay());

  if (state != SysTrayPresent)
  {
    if (wait_time-- > 0 && do_wait)
    {
      TRACE("Will check sys tray status after 1 second");
      QTimer::singleShot(1000, this, SLOT(startup()));
      return;
    }

    if (QMessageBox::warning(NULL, tr("KDocker"), 
          tr(state == SysTrayAbsent ? "No system tray found"
                                    : "System tray appears to be hidden"), 
          QMessageBox::Abort, QMessageBox::Ignore) == QMessageBox::Abort)
    {
        qApp->quit();
        return;
    }
  }

  // Things are fine or user with OK with the state of system tray
  mReady = true;
  bool ok = false;
  if (qApp->isSessionRestored()) ok = restoreSession(qApp->sessionId());
  else ok = processCommand(qApp->argc(), qApp->argv());
  // Process the request Q from previous instances

  TRACE("Request queue has %i requests", mRequestQ.count());
  for(unsigned i=0; i < mRequestQ.count(); i++)
    ok |= processCommand(mRequestQ[i]);
  if (!ok) qApp->quit();
}

// Initialize a QTrayLabel after its creation
void TrayLabelMgr::manageTrayLabel(QTrayLabel *t)
{
  connect(t, SIGNAL(destroyed(QObject *)), 
          this, SLOT(trayLabelDestroyed(QObject *)));
  connect(t, SIGNAL(undocked(QTrayLabel *)), t, SLOT(deleteLater()));

  // All QTrayLabels will emit this signal. We just need one of them
  if (mTrayLabels.count() == 0)
    connect(t, SIGNAL(sysTrayDestroyed()), this, SLOT(sysTrayDestroyed()));
  mTrayLabels.prepend(t);

  TRACE("New QTrayLabel prepended. Count=%i", mTrayLabels.count());
}

void TrayLabelMgr::dockAnother()
{
  QTrayLabel *t = selectAndDock();
  if (t == NULL) return;
  manageTrayLabel(t);
  t->withdraw();
  t->dock();
}

// Undock all the windows
void TrayLabelMgr::undockAll()
{
  TRACE("Number of tray labels = %i", mTrayLabels.count());
  QPtrListIterator<QTrayLabel> it(mTrayLabels);
  QTrayLabel *t;
  while ((t = it.current()) != 0)
  {
    ++it;
    t->undock();
  }
}

// Process the command line
bool TrayLabelMgr::processCommand(const QStringList& args)
{
  if (!mReady)
  {
    // If we are still looking for system tray, just add it to the Q
    mRequestQ.append(args);
    return true;
  }

  const int MAX_ARGS = 20;
  const char *argv[MAX_ARGS];
  int argc = args.count();
  if (argc >= MAX_ARGS) argc = MAX_ARGS - 1;

  for(int i =0 ; i<argc; i++)
    argv[i] = args[i].latin1();

  argv[argc] = NULL; // null terminate the array

  return processCommand(argc, const_cast<char **>(argv));
}

// Process the command line
bool TrayLabelMgr::processCommand(int argc, char** argv)
{
  TRACE("CommandLine arguments");
  for(int i = 0; i < argc; i++) TRACE("\t%s", argv[i]);

  if (argc < 1) return false;

  // Restore session (See the comments in KDocker::notifyPreviousInstance()
  if (qstrcmp(argv[1], "-session") == 0)
  {
    TRACE("Restoring session %s (new instance request)", argv[2]);
    return restoreSession(QString(argv[2]));
  }

  int option;
  Window w = None;
  const char *icon = NULL;
  int balloon_timeout = 4000;
  bool withdraw = true, skip_taskbar = false,
       auto_launch = false, dock_obscure = false, check_normality = true,
       enable_sm = true;

  optind = 0;        // initialise the getopt static

  while ((option = getopt(argc, argv, mOptionString)) != EOF)
  {
    switch (option)
    {
      case '?':
        return false;
      case 'a':
        qDebug(tr("Girish Ramakrishnan (gramakri@uiuc.edu)"));
        return false;
      case 'b':
        check_normality = false;
        break;
      case 'd':
        enable_sm = false;
        break;
      case 'e':
        enable_sm = true;
        break;
      case 'f':
        w = activeWindow(QPaintDevice::x11AppDisplay());
        TRACE("Active window is %i", (unsigned) w);
        break;
      case 'i':
        icon = optarg;
        break;
      case 'l':
        auto_launch = true;
        break;
      case 'm':
        withdraw = false;
        break;
      case 'o':
        dock_obscure = true;
        break;
      case 'p':
        balloon_timeout = atoi(optarg) * 1000; // convert to ms
        break;
      case 'q':
        balloon_timeout = 0; // same as '-p 0'
        break;
      case 't':
        skip_taskbar = true;
        break;
      case 'w':
        if ((optarg[1] == 'x') || (optarg[1] == 'X'))
          sscanf(optarg, "%x", (unsigned *) &w);
        else
          w = (Window) atoi(optarg);
          if (!isValidWindowId(QPaintDevice::x11AppDisplay(), w))
          {
            qDebug("Window 0x%x invalid", (unsigned) w);
            return false;
          }
        break;
     } // switch (option)
  } // while (getopt)

  // Launch an application if present in command line. else request from user
  CustomTrayLabel *t = (CustomTrayLabel *) // this should be dynamic_cast
    ((optind < argc) ? dockApplication(&argv[optind])  
		                 : selectAndDock(w, check_normality));
  if (t == NULL) return false;
  // apply settings and add to tray
  manageTrayLabel(t);
  if (icon) t->setTrayIcon(icon);
  t->setSkipTaskbar(skip_taskbar);
  t->setBalloonTimeout(balloon_timeout);
  t->setDockWhenObscured(dock_obscure);
  if (withdraw) t->withdraw(); else t->map();
  t->enableSessionManagement(enable_sm);
  t->dock();
  t->setLaunchOnStartup(auto_launch);
  return true;
}

/*
 * Request user to make a window selection if necessary. Dock the window.
 */
QTrayLabel *TrayLabelMgr::selectAndDock(Window w, bool checkNormality)
{
  if (w == None)
  {
    qDebug(tr("Select the application/window to dock with button1."));
    qDebug(tr("Click any other button to abort\n"));

    const char *err = NULL;
 
    if ((w = selectWindow(QPaintDevice::x11AppDisplay(), &err)) == None)
    {
      if (err) QMessageBox::critical(NULL, tr("KDocker"), tr(err));
      return NULL;
    }
  }

  if (checkNormality && !isNormalWindow(QPaintDevice::x11AppDisplay(), w))
  {
    /*
     * Abort should be the only option here really. "Ignore" is provided here
     * for the curious user who wants to screw himself very badly
     */
    if (QMessageBox::warning(NULL, tr("KDocker"),
          tr("The window you are attempting to dock does not seem to be a"
              " normal window."), QMessageBox::Abort,
          QMessageBox::Ignore) == QMessageBox::Abort)
      return NULL;
  }

  if (!isWindowDocked(w)) return new CustomTrayLabel(w);

  TRACE("0x%x is not docked", (unsigned) w);

  QMessageBox::message(tr("KDocker"),
    tr("This window is already docked.\n"
       "Click on system tray icon to toggle docking."));
  return NULL;
}

bool TrayLabelMgr::isWindowDocked(Window w)
{
  QPtrListIterator<QTrayLabel> it(mTrayLabels);
  for(QTrayLabel *t; (t = it.current()); ++it)
    if (t->dockedWindow() == w) return true;
 
  return false;
}

/*
 * Forks application specified by argv. Requests root window SubstructreNotify
 * notifications (for MapEvent on children). We will monitor these new windows
 * to make a pid to wid mapping (see HACKING for more details)
 */
QTrayLabel *TrayLabelMgr::dockApplication(char *argv[])
{
  pid_t pid = -1;
  int filedes[2];
  char buf[4] = { 't', 'e', 'e', 'e' }; // teeeeeee :x :-*

  /*
   * The pipe created here serves as a synchronization mechanism between the
   * parent and the child. QTrayLabel ctor keeps looking out for newly created
   * windows. Need to make sure that the application is actually exec'ed only
   * after we QTrayLabel is created (it requires pid of child)
   */
  pipe(filedes);

  if ((pid = fork()) == 0)
  {
    close(filedes[1]);
    read(filedes[0], buf, sizeof(buf));
    close(filedes[0]);

    if (execvp(argv[0], argv) == -1)
    {
      qDebug(tr("Failed to exec [%1]: %2").arg(argv[0]).arg(strerror(errno)));
      ::exit(0);                // will become a zombie in some systems :(
      return NULL;
    }
  }

  if (pid == -1)
  {
    QMessageBox::critical(NULL, "KDocker", 
                          tr("Failed to fork: %1").arg(strerror(errno)));
    return NULL;
  }

  QStringList cmd_line;
  for(int i=0;;i++)
    if (argv[i]) cmd_line << argv[i]; else break;

  QTrayLabel *label = new CustomTrayLabel(cmd_line, pid);
  qApp->syncX();
  write(filedes[1], buf, sizeof(buf));
  close(filedes[0]);
  close(filedes[1]);
  return label;
}

/*
 * Returns the number of QTrayLabels actually created but not show in the
 * System Tray
 */
int TrayLabelMgr::hiddenLabelsCount(void) const
{
  QPtrListIterator<QTrayLabel> it(mTrayLabels);
  int count = 0;
  for(QTrayLabel *t; (t=it.current()); ++it)
    if (t->dockedWindow() == None) ++count;
  return count;
}

// The number of labes that are docked in the system tray
int TrayLabelMgr::dockedLabelsCount(void) const
{
  return mTrayLabels.count() - hiddenLabelsCount();
}

void TrayLabelMgr::trayLabelDestroyed(QObject *t)
{
  bool reconnect  = ((QObject *)mTrayLabels.getLast() == t);
  mTrayLabels.removeRef((QTrayLabel*)t);
  if (mTrayLabels.isEmpty()) qApp->quit();
  else if (reconnect) 
  {
    TRACE("Reconnecting");
    connect(mTrayLabels.getFirst(), SIGNAL(sysTrayDestroyed()), 
            this, SLOT(sysTrayDestroyed()));
  }
}

void TrayLabelMgr::sysTrayDestroyed(void)
{
  /*
   * The system tray got destroyed. This could happen when it was
   * hidden/removed or killed/crashed/exited. Now we must be genteel enough
   * to not pop up a box when the user is logging out. So, we set ourselves
   * up to notify user after 3 seconds.
   */
  QTimer::singleShot(3000, this, SLOT(notifySysTrayAbsence()));
}

void TrayLabelMgr::notifySysTrayAbsence()
{
  SysTrayState state = sysTrayStatus(QPaintDevice::x11AppDisplay());

  if (state == SysTrayPresent) 
    return; // So sweet of the systray to come back so soon

  if (QMessageBox::warning(NULL, tr("KDocker"), 
                           tr("The System tray was hidden or removed"),
                           tr("Undock All"), tr("Ignore")) == 0)
    undockAll();
}

/*
 * Session Management. Always return "true". Atleast, for now
 */
bool TrayLabelMgr::restoreSession(const QString& sessionId)
{
  QString session_file = "kdocker_" + sessionId;

  QSettings settings;
  settings.beginGroup(QString("/" + session_file));

  for(int i = 1;; i++)
  {
    settings.beginGroup(QString("/Instance") + QString("").setNum(i));
    QString pname = settings.readEntry("/Application");
    TRACE("Restoring Application[%s]", pname.latin1());
    if (pname.isEmpty()) break;
    if (settings.readBoolEntry("/LaunchOnStartup"))
    {
      QStringList args("kdocker");
      args += QStringList::split(" ", pname);
      TRACE("Triggering AutoLaunch");
      if (!processCommand(args)) continue;
    }
    else 
      manageTrayLabel(new CustomTrayLabel(QStringList::split(" ", pname), 0));

    QTrayLabel *tl = mTrayLabels.getFirst(); // the one that was created above
    tl->restoreState(settings);
    settings.endGroup();
  }
 
  return true;
}

QString TrayLabelMgr::saveSession(void)
{
  QString session_file = "kdocker_" + qApp->sessionId();

  QSettings settings;
  settings.beginGroup(QString("/" + session_file));

  TRACE("Saving session");

  QPtrListIterator <QTrayLabel> it(mTrayLabels);
  QTrayLabel *t;
  int i = 1;
  while ((t = it.current()) != 0)
  {
    ++it;
    TRACE("Saving instance %i", i);
    settings.beginGroup(QString("/Instance") + QString("").setNum(i));
    bool ok = t->saveState(settings);
    settings.endGroup();
    if (ok) ++i; else TRACE("Saving of instance %i was skipped", i);
  }

  // Aaaaaaaaaaaaaa.........
  settings.removeEntry(QString("/Instance") + QString("").setNum(i));

  return QDir::homeDirPath() + "/.qt/" + session_file + "rc";
}

/*
 * The X11 Event Filter. Pass on events to the QTrayLabels that we created.
 * The logic and the code below is a bit fuzzy.
 *  a) Events about windows that are being docked need to be processed only by
 *     the QTrayLabel object that is docking that window.
 *  b) Events about windows that are not docked but of interest (like
 *     SystemTray) need to be passed on to all QTrayLabel objects.
 *  c) When a QTrayLabel manages to find the window that is was looking for, we
 *     need not process the event further
 */
bool TrayLabelMgr::x11EventFilter(XEvent *ev)
{
  QPtrListIterator<QTrayLabel> it(mTrayLabels);
  bool ret = false;
  
  // We pass on the event to all tray labels
  for(QTrayLabel *t; (t = it.current()); ++it)
  {
    Window w = t->dockedWindow();
    bool res = t->x11EventFilter(ev);
    if (w == (((XAnyEvent *)ev)->window)) return res;
    if (w != None) ret |= res;
    else if (res) return TRUE;
  }

  return ret;
}

