/*
    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 <assert.h>

#include <qregexp.h>
#include <qfile.h>
#include <qtooltip.h>
#include <qstringlist.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 <kiconloader.h>
#include <kwinmodule.h>
#include <kwin.h>
#include <ktempfile.h>
#include <kstandarddirs.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)
    : KSystemTray(parent, name),
    mPrevMenuCount(0)
{
}


void TrayWindow::setCurrentLayout(const QString& layout)
{
    QToolTip::remove(this);
    QToolTip::add(this, mDescriptionMap[layout]);

    setPixmap(findPixmap(layout));
}

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

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

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

    mDescriptionMap.clear();

    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++);
            mDescriptionMap.insert(*it, i18n((rules.layouts()[*it])));
        }
    else
        for (it=layouts.begin(); it != layouts.end(); ++it)
        {
            contextMenu()->insertItem(findPixmap(*it), i18n((rules.layouts()[*it])), cnt++, index++);
            mDescriptionMap.insert(*it, i18n((rules.layouts()[*it])));
        }    

    contextMenu()->insertItem(SmallIcon("configure"), i18n("Configure..."), cnt++, index++);
    mPrevMenuCount = 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),
    prevWinId(0),
    m_rules(0),
    m_tray(NULL),
    kWinModule(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()
{
    deletePrecompiledLayouts();
  
    delete keys;
    delete m_tray;
    delete m_rules;
    delete m_extension;
    delete kWinModule;
}

int KXKBApp::newInstance()
{
    if( !m_compiledLayoutFileNames.isEmpty() )
	deletePrecompiledLayouts();
	
    settingsRead();
    layoutApply();
    return 0;
}

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

// Even if the layouts have been disabled we still want to set Xkb options
// user can always switch them off now in the "Options" tab
    m_resetOldOptions = config->readBoolEntry("ResetOldOptions", false);
    m_options = config->readEntry("Options", "");
    m_extension->setXkbOptions(m_options, m_resetOldOptions); 

    m_enabled = config->readBoolEntry("Use", false);
    if (!m_enabled)
    {
        delete config;
	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");

    m_rules->parseVariants(vars, m_variants);

    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;
    
    precompileLayouts();
    
    if( !m_tray )
    {
	m_tray = new TrayWindow(0, 0);
	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->setCurrentLayout(m_layout);

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

    delete config;

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

// This function activates the keyboard layout specified by the
// configuration members (m_layout)
void KXKBApp::layoutApply()
{
    if (m_enabled)
        setLayout(m_layout);
    else
        kdDebug() << "control of keyboard layouts is disabled" << endl;
}

// Activates the keyboard layout specified by 'layout'
void KXKBApp::setLayout(const QString &layout)
{
    const unsigned int * pInitialGroup = m_rules->initialGroup()[layout];
    m_group = pInitialGroup ? *pInitialGroup : 0;

    if (m_compiledLayoutFileNames.contains(layout))
    {
        m_extension->setCompiledLayout(m_compiledLayoutFileNames[layout]);
        m_extension->setGroup(m_group);
    }
    else
    {
        if (m_encoding.isEmpty())
            m_encoding = "locale";
        m_extension->setLayout(m_rule, m_model, layout, m_variants[layout], m_encoding, m_group);
    }

    if (m_tray)
       m_tray->setCurrentLayout(layout);
}

// Precompiles the keyboard layouts for faster activation later.
// This is done by loading each one of them and then dumping the compiled
// map from the X server into our local buffer.
void KXKBApp::precompileLayouts()
{
    QStringList dirs = KGlobal::dirs()->findDirs ( "tmp", "" );
    QString tempDir = dirs.count() == 0 ? "/tmp/" : dirs[0]; 

    QStringList::ConstIterator end = m_list.end();

    for (QStringList::ConstIterator it = m_list.begin(); it != end; ++it)
    {
        setLayout(*it);
        QString compiledLayoutFileName = tempDir + *it + ".xkm";
        m_extension->getCompiledLayout(compiledLayoutFileName);
        m_compiledLayoutFileNames[*it]  = compiledLayoutFileName;
    }
}

// Deletes the precompiled layouts stored in temporary files
void KXKBApp::deletePrecompiledLayouts()
{
    QMapConstIterator<QString,QString> it, end;
    end = m_compiledLayoutFileNames.end();
    for (it = m_compiledLayoutFileNames.begin(); it != end; ++it)
    {
       unlink(QFile::encodeName(it.data()));
    }
    m_compiledLayoutFileNames.clear();
}

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];
    layoutApply();
}

// 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 :(
	    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
	m_layout = m_defaultLayout;
	layoutApply();
	return;
    }
	
    if( layoutInfo.layout != m_layout ) {
	m_layout = layoutInfo.layout;
        layoutApply();	// 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];
        layoutApply();
    }
    else if (id == (int)m_list.count())
    {
        KProcess p;
        p << "kcmshell" << "keyboard_layout";
        p.start(KProcess::DontCare);
    }
    else
    {
        quit();
    }
}

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

  if((XGetWindowProperty(qt_xdisplay(), w, XA_WM_CLASS, 0L, 256L, 0, XA_STRING,
			&type_ret, &format_ret, &nitems_ret,
			&bytes_after_ret, &prop_ret) == Success) && (type_ret != None)) {
    property = QString::fromLocal8Bit(reinterpret_cast<char*>(prop_ret));
    XFree(prop_ret);
  }
  return property;
}

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();
	    }
//kdDebug("getLayout: winId %lu, pid %lu, %s", winId, pid, newLayout.latin1()); 
	}
	break;

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

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

	default: assert( false );
    }
    
    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;
	    break;
	}

	case swpWindow:
	{
	    m_winLayouts[winId] = info;
	    break;
	}

	default:
	{
	    // calling this function while in Global switching mode
	    // doesn't make sense
	    assert( false );
	    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.8",
                     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();
}
