/*
    Copyright (C) 2001, S.R.Haque <srhaque@iee.org>. Derived from an
    original by Matthias Hölzer-Klüpfel released under the QPL.
    This file is part of the KDE project

    This library is free software; you can redistribute it and/or
    modify it under the terms of the GNU Library General Public
    License as published by the Free Software Foundation; either
    version 2 of the License, or (at your option) any later version.

    This library 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
    Library General Public License for more details.

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

DESCRIPTION

    KDE Keyboard Tool. Manages XKB keyboard mappings.
*/

#include <unistd.h>
#include <stdlib.h>

#include <qregexp.h>

#include <kaboutdata.h>
#include <kcmdlineargs.h>
#include <kconfig.h>
#include <kdebug.h>
#include <kglobal.h>
#include <kglobalaccel.h>
#include <klocale.h>
#include <kprocess.h>
#include <kwinmodule.h>
#include <kwin.h>

#include <X11/Xlib.h>
#include <X11/Xatom.h>
#define explicit int_explicit        // avoid compiler name clash in XKBlib.h
#include <X11/XKBlib.h>
#undef explicit

#include "kxkb.h"
#include "extension.h"
#include "rules.h"
#include "kxkb.moc"
#include "pixmap.h"


TrayWindow::TrayWindow(QWidget *parent, const char *name, const QString& currentLayout)
    : KSystemTray(parent, name),
    prevMenuCount(0)
{
    setLayout(currentLayout);
}


void TrayWindow::setLayout(const QString& layout)
{
    setPixmap(findPixmap(layout));
}


static QString lookup(const QDict<char> &dict, const QString& text)
{
    QDictIterator<char> it(dict);
    while (it.current())
    {
        if (it.current() == text)
            return it.currentKey();
        ++it;
    }
    return QString::null;
}

void TrayWindow::setLayouts(const QStringList& layouts, const QString& rule)
{
    KeyRules rules(rule);

    int index = contextMenu()->indexOf(0);

    // we cannot clear hole menu because there're preset Title and Quit item etc..
    // so we have to remove previous layouts
    for(int id=0; id<prevMenuCount; id++)
	contextMenu()->removeItem(id);

    int cnt=0;
    QStringList::ConstIterator it;
    if( index == -1 )
    for (it=layouts.begin(); it != layouts.end(); ++it)
        contextMenu()->insertItem(findPixmap(*it), i18n((rules.layouts()[*it])), cnt++);
    else
    for (it=layouts.begin(); it != layouts.end(); ++it)
        contextMenu()->insertItem(findPixmap(*it), i18n((rules.layouts()[*it])), cnt++, index++);
    
    contextMenu()->insertItem(i18n("Configure..."), cnt++, index++);
    prevMenuCount = cnt;
}

void TrayWindow::mouseReleaseEvent(QMouseEvent *ev)
{
    if (ev->button() == QMouseEvent::LeftButton)
        emit toggled();
    KSystemTray::mouseReleaseEvent(ev);
}



KXKBApp::KXKBApp(bool allowStyles, bool GUIenabled)
    : KUniqueApplication(allowStyles, GUIenabled),
    kWinModule(0),
    prevWinId(0),
    m_rules(0)
{
    // Verify the Xlib has matching XKB extension.

    int major = XkbMajorVersion;
    int minor = XkbMinorVersion;
    if (!XkbLibraryVersion(&major, &minor))
    {
        kdError() << "Xlib XKB extension " << major << '.' << minor <<
            " != " << XkbMajorVersion << '.' << XkbMinorVersion << endl;
        exit(-1);
    }

    // Verify the X server has matching XKB extension.

    int opcode_rtrn;
    int error_rtrn;
    int xkb_opcode;
    if (!XkbQueryExtension(qt_xdisplay(), &opcode_rtrn, &xkb_opcode, &error_rtrn,
                         &major, &minor))
    {
        kdError() << "X server XKB extension " << major << '.' << minor <<
            " != " << XkbMajorVersion << '.' << XkbMinorVersion << endl;
        exit(-1);
    }

    // Now the XKB extension is initialized.

    m_extension = new XKBExtension;
    m_tray = 0;
    // keep in sync with kcmlayout.cpp
    keys = new KGlobalAccel(this);
#include "kxkbbindings.cpp"
    keys->updateConnections();
}


KXKBApp::~KXKBApp()
{
    delete keys;
    delete m_tray;
    delete m_rules;
    delete m_extension;
    delete kWinModule;
}

int KXKBApp::newInstance()
{
    settingsRead();
    settingsApply();
    return 0;
}

void KXKBApp::settingsRead()
{
    KConfig *config = new KConfig("kxkbrc", true);
    config->setGroup("Layout");

    // If we have been disabled, we should just quit right away to avoid the
    // possibility that the user is using some other tool to manage layouts: we
    // do NOT want to risk calling setxkbmap, otherwise user settings of xkb
    // (set e.g. in XF86Config) will be overwritten.

    m_enabled = config->readBoolEntry("Use", false);
    if (!m_enabled)
    {
	kapp->quit();
        return;
    }

    QString layoutOwner = config->readEntry("SwitchMode", "Global");
    

    if( layoutOwner != "WinClass" && layoutOwner != "Window" ) {
	m_layoutOwnerMap.setMode(swpGlobal);
	delete kWinModule;
	kWinModule = 0;
    }
    else {
	if( !kWinModule ) {
	    kWinModule = new KWinModule();
	    connect(kWinModule, SIGNAL(activeWindowChanged(WId)), SLOT(windowChanged(WId)));
	}
	if( layoutOwner == "WinClass" ) {
	    m_layoutOwnerMap.setMode(swpWinClass);
	}
	else if( layoutOwner == "Window" ) {
	    m_layoutOwnerMap.setMode(swpWindow);
	}
    }
    

    m_rule = config->readEntry("Rule", "xfree86");
    m_rules = new KeyRules( m_rule );
    m_model = config->readEntry("Model", "pc104");
    m_layout = config->readEntry("Layout", "");
    m_defaultLayout = m_layout;


// reading variants
  QStringList vars = config->readListEntry("Variants");

  static const char* LAYOUT_PATTERN = "[a-z0-9_]*";
  static const char* VARIANT_PATTERN = "\\([a-z0-9_]*\\)";
  for (QStringList::Iterator it = vars.begin(); it != vars.end(); ++it)
  {
      QString varLine = (*it).stripWhiteSpace();
      QRegExp rx(LAYOUT_PATTERN);
      int pos = rx.search(varLine, 0);
      int len = rx.matchedLength();
  // check for errors
      if( pos < 0 || len < 2 )
        continue;
      QString layout = varLine.mid(pos, len);
      rx.setPattern(VARIANT_PATTERN);
      pos = rx.search(varLine, pos+len);
      len = rx.matchedLength();
  // check for errors
      if( pos < 2 || len < 3 )
        continue;
      QString variant = varLine.mid(pos+1, len-2);


      QStringList addVars = m_rules->getVariants(layout);
      if( !variant.isEmpty() && addVars.contains(variant) )
      {
        m_variants.insert(layout, strdup(variant.latin1()));
      }
  }

  m_resetOldOptions = config->readBoolEntry("ResetOldOptions", false);
  m_options = config->readEntry("Options", "");
  
    m_encoding = config->readEntry("Encoding", "locale");
    m_list = config->readListEntry("Additional");
    m_encList = config->readListEntry("AdditionalEncodings");
    if (!m_list.contains(m_layout))
    {
        m_list.append(m_layout);
        m_encList.append(m_encoding);
    }

    // Note that we want to proceed even if there is only one entry in the list.
    // This at least gives the user the chance to configure more from using our
    // menu.

    kdDebug() << "found " << m_list.count() << " layouts" << endl;

    if( !m_tray )
    {
	m_tray = new TrayWindow(0, 0, m_layout);
	connect(m_tray->contextMenu(), SIGNAL(activated(int)), this, SLOT(menuActivated(int)));
	connect(m_tray, SIGNAL(toggled()), this, SLOT(toggled()));

	m_tray->setLayouts(m_list, m_rule);
	m_tray->show();
    }
    else
    {
	m_tray->setLayout(m_layout);
	m_tray->setLayouts(m_list, m_rule);
    }

    delete config;

    KGlobal::config()->reparseConfiguration(); // kcontrol modified kdeglobals
    keys->readSettings();
    keys->updateConnections();
}

void KXKBApp::settingsApply()
{
    // If we have been enabled, apply the chosen settings.
    
    if (m_enabled)
    {
        m_tray->setLayout(m_layout);
        if (m_encoding.isEmpty())
            m_encoding = "locale";
        const unsigned int * pGroup = m_rules->group()[m_layout];
        m_group = pGroup ? *pGroup : 0;
	
        m_extension->setLayout(m_rule, m_model, m_layout, m_variants[m_layout],
		     m_options, m_resetOldOptions, m_encoding, m_group);
    }
    else
    {
        kdDebug() << "control of keyboard layouts is disabled" << endl;
    }
}

void KXKBApp::toggled()
{
    unsigned int index = m_list.findIndex(m_layout) + 1;
    if (index >= m_list.count())
        index = 0;
    m_layout = m_list[index];
    m_encoding = m_encList[index];
    settingsApply();
}

//#include <fstream.h>

//ofstream of("/tmp/kxkb.out");

// we also have to handle deleted windows

void KXKBApp::windowChanged(WId winId)
{
    if( m_layoutOwnerMap.getMode() == swpGlobal )	// should not happen actually
	return;
    

    int group = m_extension->getGroup();
    
    if( prevWinId ) {	// saving layout/group from previous window
			// this will not work for the window activated before kxkb start :(
	// if( m_layout != m_defaultLayout || group != groups[m_layout] )
//of << "win change: " << prevWinId << " " << m_layout.latin1() << " " << group << endl;
	    LayoutInfo layoutInfo(m_layout, group);
	    m_layoutOwnerMap.setLayout(prevWinId, layoutInfo);
    }
    
    prevWinId = winId;

    const LayoutInfo& layoutInfo = m_layoutOwnerMap.getLayout(winId);
    
    if( layoutInfo.layout.isEmpty() ) {	// setting default layout/group
//of << winId << " empty" << endl;
	m_layout = m_defaultLayout;
	settingsApply();
	return;
    }
	
    if( layoutInfo.layout != m_layout ) {
//of << winId << " "  << layoutInfo.layout.latin1() << endl;
	m_layout = layoutInfo.layout;
        settingsApply();	// we have to add group parameter to settingApply() ??
	m_extension->setGroup(layoutInfo.group);
    }
    else if( layoutInfo.group != group ) {	// we need to change only the group
	m_extension->setGroup(layoutInfo.group);
    }
    // nothing to do
}

void KXKBApp::menuActivated(int id)
{
    if (0 <= id && id < (int)m_list.count())
    {
        m_layout = m_list[id];
        m_encoding = m_encList[id];
        settingsApply();
    }
    else if (id == (int)m_list.count())
    {
        KProcess p;
        p << "kcmshell" << "keyboard";
        p.start(KProcess::DontCare);
    }
    else
    {
        quit();
    }
}

static QString windowClass(WId winId)
{
  unsigned long nitems_ret, bytes_after_ret;
  unsigned char* prop;
  Atom     type_ret;
  int      format_ret;
  Window w = (Window)winId;	// suppose WId == Window
  QString  ret = "";

  if(XGetWindowProperty(qt_xdisplay(), w, XA_WM_CLASS, 0L, 256L, 0, XA_STRING,
			&type_ret, &format_ret, &nitems_ret,
			&bytes_after_ret, &prop) == Success
     && type_ret != None) {
    ret = (const char*)prop;
    XFree((char*)prop);
  }
  return ret;
}

SwitchingPolicy LayoutMap::getMode() 
{
    return m_ownerMode;
}


void LayoutMap::setMode(SwitchingPolicy mode)
{
    m_ownerMode = mode;

    m_appLayouts.clear();
    m_winLayouts.clear();
}


const LayoutInfo& LayoutMap::getLayout(WId winId)
{

    static LayoutInfo emptyInfo;

    // we should not et here with mode==Global
    switch( m_ownerMode ) {

	case swpWinClass:
	{
	    QString winClass = windowClass(winId);
	    WinClassLayoutMap::Iterator it = m_appLayouts.find(winClass);

	    if( it == m_appLayouts.end() ) {
		return emptyInfo; //m_defaultLayout;
	    }
	    else {
		return it.data();
	    }
//qDebug("getLayout: winId %lu, pid %lu, %s", winId, pid, newLayout.latin1()); 
//of << "getLayout: winId " << winId << ", pid " << pid << ", " <<  newLayout.latin1() << endl; 
	}
	break;

	case swpWindow:
	{
	    WinLayoutMap::Iterator it = m_winLayouts.find(winId);

	    if( it == m_winLayouts.end() ) {
		return emptyInfo; //m_defaultLayout;
	    }
	    else {
		return it.data();
	    }
	}
	break;
    }
    
    return emptyInfo;
}


void LayoutMap::setLayout(WId winId, const LayoutInfo& info)
{
//    LayoutInfo& layoutInfo;
    switch( m_ownerMode ) {

	case swpWinClass:
	{
	    QString winClass = windowClass(winId);
	    m_appLayouts[winClass] = info;
//qDebug("setLayout: winId %lu, pid %lu, %s", winId, pid, layout.latin1()); 
//of << "getLayout: winId " << winId << ", pid " << pid << ", " <<  layout.latin1() << endl; 
	}
	break;

	case swpWindow:
	    m_winLayouts[winId] = info;
//	    m_winLayouts[winId].layout = info.layout;
//	    m_winLayouts[winId].group = info.group;
	break;
    }
}


const char * DESCRIPTION =
  I18N_NOOP("A utility to switch keyboard maps.");

int main(int argc, char *argv[])
{
    KAboutData about("kxkb", I18N_NOOP("KDE Keyboard Tool"), "0.6",
                     DESCRIPTION, KAboutData::License_LGPL,
                     "Copyright (C) 2001, S.R.Haque");
    KCmdLineArgs::init(argc, argv, &about);
    KXKBApp::addCmdLineOptions();

    if (!KXKBApp::start())
        return 0;

    KXKBApp app;
    app.disableSessionManagement();
    app.exec();
}
