/* 
 *  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 <tqfile.h>
#include <tqtextstream.h>

#include <tdeapplication.h>
#include <tdecmdlineargs.h>
#include <tdeconfig.h>
#include <tdelocale.h>
#include <tdemessagebox.h>

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

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

TrayLabelMgr* TrayLabelMgr::gTrayLabelMgr = NULL;

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

TrayLabelMgr::TrayLabelMgr() : mReady(false), mHiddenLabelsCount(0)
{
  connect(&restoreSessionTimer, TQ_SIGNAL(timeout()), this, TQ_SLOT(doRestoreSession()));

  // Set ourselves up to be called from the application loop
  TQTimer::singleShot(0, this, TQ_SLOT(startup()));
}

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

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)) || tdeApp->isRestored();

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

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

    if (KMessageBox::warningContinueCancel(NULL,
          state == SysTrayAbsent ? i18n("No system tray found") : i18n("System tray appears to be hidden"),
          i18n("TDEDocker")) == KMessageBox::Cancel)
    {
        tdeApp->quit();
        return;
    }
  }

  // Things are fine or user with OK with the state of system tray
  mReady = true;
  bool ok = false;
  if (tdeApp->isRestored())
  {
    restoreSession();
    ok = true;
  }
  else
  {
    ok = processCommand(TDECmdLineArgs::parsedArgs());
  }

  // 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)
  {
    tdeApp->quit();
  }
}

// Initialize a TQTrayLabel after its creation
void TrayLabelMgr::manageTrayLabel(TQTrayLabel *t)
{
  connect(t, TQ_SIGNAL(destroyed(TQObject *)), this, TQ_SLOT(trayLabelDestroyed(TQObject *)));
  connect(t, TQ_SIGNAL(undocked(TQTrayLabel *)), t, TQ_SLOT(deleteLater()));

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

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

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

// Close all the windows and quit
void TrayLabelMgr::quitAll()
{
  TRACE("quitAll: number of tray labels = %i", mTrayLabels.count());
  TQPtrListIterator<TQTrayLabel> it(mTrayLabels);
  TQTrayLabel *t;
  while ((t = it.current()) != 0)
  {
    ++it;
    t->close();
  }
}

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

// Process the command line
bool TrayLabelMgr::processCommand(const TQStringList &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 = 50;
  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].local8Bit();
  }

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

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

// Process the command line
bool TrayLabelMgr::processCommand(TDECmdLineArgs *args)
{
  TQStringList argl;
  argl.append(TDECmdLineArgs::appName());

  if (args->isSet("b"))
  {
    argl.append("-b");
  }
  if (args->isSet("d"))
  {
    argl.append("-d");
  }
  if (args->isSet("e"))
  {
    argl.append("-e");
  }
  if (args->isSet("f"))
  {
    argl.append("-f");
  }
  if (args->isSet("i"))
  {
    argl.append("-i");
    argl.append(args->getOption("i"));
  }
  if (args->isSet("m"))
  {
    argl.append("-m");
  }
  if (args->isSet("o"))
  {
    argl.append("-o");
  }
  if (args->isSet("p"))
  {
    argl.append("-p");
    argl.append(args->getOption("p"));
  }
  if (args->isSet("q"))
  {
    argl.append("-q");
  }
  if (args->isSet("t"))
  {
    argl.append("-t");
  }
  if (args->isSet("w"))
  {
    argl.append("-w");
    argl.append(args->getOption("w"));
  }

  for (int i = 0; i < args->count(); ++i)
  {
    argl.append(args->arg(i));
  }

  return processCommand(argl);
}

// 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 TDEDocker::notifyPreviousInstance() )
  if (qstrcmp(argv[1], "--restore-internal") == 0)
  {
    TRACE("Restoring session (new instance request)");
    restoreSession();
    return true;
  }

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

  optind = 0;        // initialise the getopt static

  while ((option = getopt(argc, argv, "+bdefi:lmop:qtw:")) != EOF)
  {
    switch (option)
    {
      case '?':
        return false;
      case 'b':
        check_normality = false;
        break;
      case 'd':
        enable_sm = false;
        break;
      case 'e':
        enable_sm = true;
        break;
      case 'f':
        w = activeWindow(TQPaintDevice::x11AppDisplay());
        TRACE("Active window is %i", (unsigned) w);
        break;
      case 'i':
        icon = optarg;
        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(TQPaintDevice::x11AppDisplay(), w))
          {
            tqDebug("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
  TQTrayLabel *t = (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->setSessionManagement(enable_sm);
  t->dock();
  return true;
}

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

    const char *err = NULL;

    if ((w = selectWindow(TQPaintDevice::x11AppDisplay(), &err)) == None)
    {
      if (err)
      {
        KMessageBox::error(NULL, err, i18n("TDEDocker"));
      }
      return NULL;
    }
  }

  if (checkNormality && !isNormalWindow(TQPaintDevice::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 (KMessageBox::warningContinueCancel(NULL,
          i18n("The window you are attempting to dock does not seem to be a normal window."),
          i18n("TDEDocker")) == KMessageBox::Cancel)
    {
      return NULL;
    }
  }

  if (!isWindowDocked(w))
  {
    return new TQTrayLabel(w);
  }

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

  KMessageBox::error(NULL, i18n("This window is already docked.\n"
       "Click on system tray icon to toggle docking."), i18n("TDEDocker"));
  return NULL;
}

bool TrayLabelMgr::isWindowDocked(Window w)
{
  TQPtrListIterator<TQTrayLabel> it(mTrayLabels);
  for(TQTrayLabel *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)
 */
TQTrayLabel *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. TQTrayLabel ctor keeps looking out for newly created
   * windows. Need to make sure that the application is actually exec'ed only
   * after we TQTrayLabel 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)
    {
      tqDebug("%s", i18n("Failed to exec [%1]: %2").arg(argv[0]).arg(strerror(errno)).local8Bit().data());
      // Exit the forked process only.
      // Using tdeApp->quit() crashes the parent application.
      exit(0);
      return NULL;
    }
  }

  if (pid == -1)
  {
    KMessageBox::error(NULL, i18n("Failed to fork: %1").arg(strerror(errno)), i18n("Ignore"));
    return NULL;
  }

  TQStringList cmd_line;
  int i = 0;
  while (argv[i])
  {
    cmd_line.append(argv[i]);
    ++i;
  }

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

/*
 * Returns the number of TQTrayLabels actually created but not show in the
 * System Tray
 */
int TrayLabelMgr::hiddenLabelsCount(void) const
{
  TQPtrListIterator<TQTrayLabel> it(mTrayLabels);
  int count = 0;
  for(TQTrayLabel *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(TQObject *t)
{
  bool reconnect  = ((TQObject *)mTrayLabels.getLast() == t);
  mTrayLabels.removeRef((TQTrayLabel*)t);
  if (mTrayLabels.isEmpty())
  {
    tdeApp->quit();
  }
  else if (reconnect) 
  {
    TRACE("Reconnecting");
    connect(mTrayLabels.getFirst(), TQ_SIGNAL(sysTrayDestroyed()), 
            this, TQ_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.
   */
  TQTimer::singleShot(3000, this, TQ_SLOT(notifySysTrayAbsence()));
}

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

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

  KMessageBox::error(NULL, i18n("The System tray was hidden or removed. All applications "
                     "will be undocked."), i18n("TDEDocker"));
  undockAll();
}

void TrayLabelMgr::restoreSession()
{
  // After restoring a session, the TDE session manager will relaunch the applications
  // that were previously docked before terminating the previous session. To avoid
  // launching the same apps twice, wait for a while before restoring the tdedocker
  // session, so that we give tdedocker a chance to simply docks the applications
  // already launched by TDE session manager.
  restoreSessionTimer.start(5000, true);
}

void TrayLabelMgr::doRestoreSession()
{
  TRACE("Restoring session");

  TDEConfig *config = tdeApp->sessionConfig();
  if (config->hasGroup("General"))
  {
    config->setGroup("General");
    int count = config->readNumEntry("InstanceCount", 0);
    for (int i = 0; i < count; ++i)
    {
      if (!config->hasGroup(TQString::number(i)))
      {
        break;
      }
      config->setGroup(TQString::number(i));
      TQString pname = config->readEntry("Application", TQString::null);
      if (!pname.isEmpty())
      {
        TRACE("Restoring Application[%s]", pname.ascii());
        manageTrayLabel(new TQTrayLabel(TQStringList::split(" ", pname), 0));
        if (!mTrayLabels.getFirst()->restoreState(config))
        {
          // Failed to restore the application, remove the tray label
          delete mTrayLabels.take(0);
        }
      }
    }
  }
  // Exit if no application could be restored
  if (mTrayLabels.isEmpty())
  {
    tdeApp->quit();
  }
}

bool TrayLabelMgr::saveState(TQSessionManager &sm)
{
  TRACE("Saving session");

  int i = 0;
  TQTrayLabel *t;
  TDEConfig *config = tdeApp->sessionConfig();
  TQPtrListIterator<TQTrayLabel> it(mTrayLabels);
  for (it.toFirst(); it.current(); ++it)
  {
    t = it.current();
    config->setGroup(TQString::number(i));
    if (t->saveState(config))
    {
      TRACE("Saved instance %i", i);
      ++i;
    }
    else
    {
      config->deleteGroup(TQString::number(i));
    }
  }

  config->setGroup("General");
  config->writeEntry("InstanceCount", i);
  return true;
}

/*
 * The X11 Event Filter. Pass on events to the TQTrayLabels 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 TQTrayLabel 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 TQTrayLabel objects.
 *  c) When a TQTrayLabel manages to find the window that is was looking for, we
 *     need not process the event further
 */
bool TrayLabelMgr::x11EventFilter(XEvent *ev)
{
  TQPtrListIterator<TQTrayLabel> it(mTrayLabels);
  bool ret = false;

  // We pass on the event to all tray labels
  for(TQTrayLabel *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;
}


#include "traylabelmgr.moc"
