/*
    Copyright (C) 2001, S.R.Haque <srhaque@iee.org>. Derived from an
    original by Matthias H�zer-Klpfel 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., 51 Franklin Street, Fifth Floor,
    Boston, MA 02110-1301, USA.

DESCRIPTION

    KDE Keyboard Tool. Manages XKB keyboard mappings.
*/

#include <unistd.h>
#include <assert.h>

#include <tqregexp.h>
#include <tqfile.h>
#include <tqstringlist.h>
#include <tqimage.h>
#include <tqtimer.h>

#include <tdeaboutdata.h>
#include <tdecmdlineargs.h>
#ifdef WITH_TDEHWLIB
#include <tdehardwaredevices.h>
#endif
#include <tdeglobal.h>
#include <kglobalaccel.h>
#include <tdelocale.h>
#include <kprocess.h>
#include <twinmodule.h>
#include <twin.h>
#include <tdetempfile.h>
#include <kstandarddirs.h>
#include <kipc.h>
#include <tdeaction.h>
#include <tdepopupmenu.h>
#include <kdebug.h>
#include <tdeconfig.h>
#include <knotifyclient.h>
#include <dcopclient.h>
#include <dcopref.h>

#include "x11helper.h"
#include "kxkb.h"
#include "extension.h"
#include "rules.h"
#include "kxkbconfig.h"
#include "layoutmap.h"

#include "kxkb.moc"


KXKBApp::KXKBApp(bool allowStyles, bool GUIenabled)
    : KUniqueApplication(allowStyles, GUIenabled),
    m_prevWinId(X11Helper::UNKNOWN_WINDOW_ID),
    m_rules(nullptr),
    m_tray(nullptr),
    kWinModule(nullptr)
{
	X11Helper::initializeTranslations();
	XKBExtension *xkb = XKBExtension::the();
	connect(xkb, TQ_SIGNAL(groupChanged(uint)), this, TQ_SLOT(slotGroupChanged(uint)));
	connect(xkb, TQ_SIGNAL(optionsChanged()), this, TQ_SLOT(slotSyncXkbOptions()));

	m_layoutOwnerMap = new LayoutMap(kxkbConfig);

	// keep in sync with kcmlayout.cpp
	keys = new TDEGlobalAccel(this);
#include "kxkbbindings.cpp"

    connect( this, TQ_SIGNAL(settingsChanged(int)), TQ_SLOT(slotSettingsChanged(int)) );
    addKipcEventMask( KIPC::SettingsChanged );

#if WITH_TDEHWLIB
    TDEHardwareDevices *hwdevices = TDEGlobal::hardwareDevices();
    connect(hwdevices, TQ_SIGNAL(hardwareAdded(TDEGenericDevice*)), this, TQ_SLOT(hardwareChanged(TDEGenericDevice*)));
    connect(hwdevices, TQ_SIGNAL(hardwareRemoved(TDEGenericDevice*)), this, TQ_SLOT(hardwareChanged(TDEGenericDevice*)));
    connect(hwdevices, TQ_SIGNAL(hardwareUpdated(TDEGenericDevice*)), this, TQ_SLOT(hardwareChanged(TDEGenericDevice*)));
#endif
}

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

int KXKBApp::newInstance()
{
    readSettings();
    return 0;
}

void KXKBApp::readSettings()
{
    // Xkb options
    kxkbConfig.load(KxkbConfig::LOAD_INIT_OPTIONS);

    if (!kxkbConfig.m_useKxkb)
    {
        kdDebug() << "kxkb is disabled, applying xkb options and exiting" << endl;
        applyXkbOptions();
        quit();
        return;
    }

    kdDebug() << "applying xkb options and layouts" << endl;
    kxkbConfig.load(KxkbConfig::LOAD_ALL_OPTIONS);
    applyXkbOptions();

    // Active window watcher
    m_prevWinId = X11Helper::UNKNOWN_WINDOW_ID;

    if (kxkbConfig.m_switchingPolicy == SWITCH_POLICY_GLOBAL)
    {
        delete kWinModule;
        kWinModule = nullptr;
    }

    else
    {
        TQDesktopWidget desktopWidget;
        if (desktopWidget.numScreens() > 1 && !desktopWidget.isVirtualDesktop())
        {
            kdWarning() << "With non-virtual desktop only global switching policy supported on non-primary screens" << endl;
            //TODO: find out how to handle that
        }

        if (!kWinModule)
        {
            kWinModule = new KWinModule(nullptr, KWinModule::INFO_DESKTOP);
            connect(kWinModule, TQ_SIGNAL(activeWindowChanged(WId)), TQ_SLOT(windowChanged(WId)));
        }

        m_prevWinId = kWinModule->activeWindow();
        kdDebug() << "Active window " << m_prevWinId << endl;
    }

    // Init layout owner map
    m_layoutOwnerMap->reset();
    m_layoutOwnerMap->setCurrentWindow( m_prevWinId );

    // Init rules
    if (!m_rules)
    {
        m_rules = new XkbRules(false);
    }

    // Init layouts
    for (int i = 0; i < kxkbConfig.m_layouts.count(); i++)
    {
        LayoutUnit& layoutUnit = kxkbConfig.m_layouts[i];
    }

    m_currentLayout = kxkbConfig.m_layouts[0];
    setLayout(m_currentLayout);

    kdDebug() << "default layout is " << m_currentLayout.toPair() << endl;

    if (kxkbConfig.m_layouts.count() == 1 && !kxkbConfig.m_showSingle)
    {
        quit();
        return;
    }

    TDEGlobal::config()->reparseConfiguration(); // kcontrol modified kdeglobals

    // Init tray
    if (!m_tray)
    {
        m_tray = new KxkbSystemTray(&kxkbConfig);
        connect(m_tray, TQ_SIGNAL(menuActivated(int)), this, TQ_SLOT(menuActivated(int)));
        connect(m_tray, TQ_SIGNAL(toggled()), this, TQ_SLOT(nextLayout()));
    }

    m_tray->initLayoutList(kxkbConfig.m_layouts, *m_rules);
    m_tray->setCurrentLayout(m_currentLayout);
    m_tray->show();

    // Init keybindings
    keys->readSettings();
    keys->updateConnections();
}

void KXKBApp::applyXkbOptions()
{
    XkbOptions options = kxkbConfig.getKXkbOptions();
    if (!XKBExtension::the()->setXkbOptions(options)) {
        kdWarning() << "Setting XKB options failed!" << endl;
    }
}

void KXKBApp::hardwareChanged(TDEGenericDevice *dev)
{
#   if WITH_TDEHWLIB
    if (dev->type() == TDEGenericDeviceType::Keyboard)
    {
        TQTimer::singleShot(500, this, TQ_SLOT(applyXkbOptions()));
    }
#   endif
}

// kdcop
bool KXKBApp::setLayout(const TQString& layoutPair)
{
    return setLayout((LayoutUnit)layoutPair);
}

// Activates the keyboard layout specified by 'layoutUnit'
bool KXKBApp::setLayout(const LayoutUnit& layoutUnit)
{
    const int group = kxkbConfig.m_layouts.findIndex(layoutUnit);
    if (group >= 0) {
        return setLayout(group);
    }
    return false;
}

// Activates the keyboard layout specified by group number
bool KXKBApp::setLayout(const uint group)
{
    // If this group is already set, just show the notification and return
    if (XKBExtension::the()->getGroup() == group) {
        if (kxkbConfig.m_enableNotify) {
            showLayoutNotification();
        }
        return true;
    }

    bool ok = XKBExtension::the()->setGroup(group);
    if (!ok) {
        TQString layout = kxkbConfig.m_layouts[group].toPair();
        if (m_tray) {
            m_tray->setError(layout);
        }

        if (kxkbConfig.m_enableNotify) {
            showErrorNotification(layout);
        }
    }
    return ok;
}

void KXKBApp::nextLayout()
{
    const LayoutUnit& layout = m_layoutOwnerMap->getNextLayout().layoutUnit;
    setLayout(layout);
}

void KXKBApp::prevLayout()
{
    const LayoutUnit& layout = m_layoutOwnerMap->getPrevLayout().layoutUnit;
    setLayout(layout);
}

void KXKBApp::menuActivated(int id)
{
    if (id >= KxkbSystemTray::START_MENU_ID &&
        id <  KxkbSystemTray::START_MENU_ID + kxkbConfig.m_layouts.count())
    {
        setLayout(id - KxkbSystemTray::START_MENU_ID);
    }
    else if (id == KxkbSystemTray::CONFIG_MENU_ID)
    {
        TDEProcess p;
        p << "tdecmshell" << "keyboard_layout";
        p.start(TDEProcess::DontCare);
    }
    else if (id == KxkbSystemTray::HELP_MENU_ID)
    {
        invokeHelp(0, "kxkb");
    }
    else
    {
        quit();
    }
}

void KXKBApp::slotGroupChanged(uint group)
{
    if (!kxkbConfig.m_layouts.count()) {
      kdError() << "[kxkb] no layout found!" << endl;
      return;
    }

    if (group >= kxkbConfig.m_layouts.count()) {
        kdError() << "[kxkb] unknown group requested: " << group << endl;
        if (m_tray)
        {
            m_tray->setError(i18n("Unknown"));
        }
        if (kxkbConfig.m_enableNotify)
        {
            showErrorNotification(i18n("Unknown"));
        }
        return;
    }

    m_currentLayout = kxkbConfig.m_layouts[group];
    m_layoutOwnerMap->setCurrentLayout(m_currentLayout);

    if (m_tray) {
        m_tray->setCurrentLayout(m_currentLayout);
    }

    if (kxkbConfig.m_enableNotify) {
        showLayoutNotification();
    }
}

void KXKBApp::slotSyncXkbOptions()
{
	// Make sure the X11 server has had enough time to apply the change
	TQTimer::singleShot(100, this, TQ_SLOT(syncXkbOptions()));
}

void KXKBApp::syncXkbOptions()
{
	XkbOptions options = XKBExtension::the()->getServerOptions();
	if (kxkbConfig.setFromXkbOptions(options))
	{
		m_layoutOwnerMap->reset();
		if (m_tray)
		{
			m_tray->initLayoutList(kxkbConfig.m_layouts, *m_rules);
		}
	}
	slotGroupChanged(XKBExtension::the()->getGroup());
}

void KXKBApp::showLayoutNotification()
{
    bool useKMilo = kxkbConfig.m_notifyUseKMilo && isKMiloAvailable(),
         notificationSent = false;

    TQString layoutName(m_rules->getLayoutName(m_currentLayout));

    if (useKMilo) {
        DCOPRef kmilo("kded", "kmilod");
        if (kmilo.send("displayText(TQString,TQPixmap)", layoutName, miniIcon())) {
            notificationSent = true;
        }
    }

    if (!notificationSent) {
        WId wid = (m_tray ? m_tray->winId() : 0);
        KNotifyClient::event(wid, "LayoutChange", layoutName);
    }
}

void KXKBApp::showErrorNotification(TQString layout) {
    bool useKMilo = kxkbConfig.m_notifyUseKMilo && isKMiloAvailable(),
         notificationSent = false;

    if (useKMilo) {
        DCOPRef kmilo("kded", "kmilod");
        if (kmilo.send("displayText(TQString,TQPixmap)", i18n("Error changing keyboard layout to '%1'").arg(layout), miniIcon())) {
            notificationSent = true;
        }
    }

    if (!notificationSent) {
        WId wid = (m_tray ? m_tray->winId() : 0);
        KNotifyClient::event(wid, "Error");
    }
}

bool KXKBApp::isKMiloAvailable() {
    QCStringList modules;
    TQCString replyType;
    TQByteArray replyData;
    if (dcopClient()->call("kded", "kded", "loadedModules()",
                                 TQByteArray(), replyType, replyData))
    {
        if (replyType == "QCStringList") {
            TQDataStream reply(replyData, IO_ReadOnly);
            reply >> modules;
            return modules.contains("kmilod");
        }
    }
    return false;
}

// TODO: we also have to handle deleted windows
void KXKBApp::windowChanged(WId winId)
{
//	kdDebug() << "window switch" << endl;
	if( kxkbConfig.m_switchingPolicy == SWITCH_POLICY_GLOBAL ) { // should not happen actually
		kdDebug() << "windowChanged() signal in GLOBAL switching policy" << endl;
		return;
	}

	kdDebug() << "old WinId: " << m_prevWinId << ", new WinId: " << winId << endl;

	if( m_prevWinId != X11Helper::UNKNOWN_WINDOW_ID ) {	// saving layout from previous window
// 		m_layoutOwnerMap->setCurrentWindow(m_prevWinId);
		m_layoutOwnerMap->setCurrentLayout(m_currentLayout);
	}

	m_prevWinId = winId;

	if( winId != X11Helper::UNKNOWN_WINDOW_ID ) {
		m_layoutOwnerMap->setCurrentWindow(winId);
		const LayoutState& layoutState = m_layoutOwnerMap->getCurrentLayout();

		if( layoutState.layoutUnit != m_currentLayout ) {
			kdDebug() << "switching to " << layoutState.layoutUnit.toPair() << " for "  << winId << endl;
			setLayout(layoutState.layoutUnit);
		}
	}
}

void KXKBApp::slotSettingsChanged(int category)
{
	if (category == TDEApplication::SETTINGS_SHORTCUTS) {
		TDEGlobal::config()->reparseConfiguration(); // kcontrol modified kdeglobals
		keys->readSettings();
		keys->updateConnections();
    }
}

bool KXKBApp::x11EventFilter(XEvent *e) {
    // let the extension process the event and emit signals if necessary
    XKBExtension::the()->processXEvent(e);
    return TDEApplication::x11EventFilter(e);
}

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

extern "C" TDE_EXPORT int kdemain(int argc, char *argv[])
{
    TDEAboutData about("kxkb", I18N_NOOP("TDE Keyboard Tool"), "1.0",
                     DESCRIPTION, TDEAboutData::License_LGPL,
                     "Copyright (C) 2001, S.R.Haque\n(C) 2002-2003, 2006 Andriy Rysin");
    TDECmdLineArgs::init(argc, argv, &about);
    KXKBApp::addCmdLineOptions();

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

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