/*
 Keeps per window keyboard layout.
 Copyright (C) 2011 Anton Butanaev <anton.butanaev@gmail.com>

 This program 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 3 of the License, or
 (at your option) any later version.

 This program 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 program.  If not, see <http://www.gnu.org/licenses/>.
 */
#include <iostream>
#include <map>
#include <fstream>

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

#include <X11/X.h>
#include <X11/Xlib.h>
#include <X11/XKBlib.h>
#include <X11/Xatom.h>

using namespace std;

#if defined(DEBUG2)

string logFile = string(getenv("HOME")) + "/.perWindowLayoutD.log";
ofstream log(logFile.c_str(), ios::app);
#define LOG(x) log << x << "\n"; log.flush();

#elif defined(DEBUG)

#define LOG(x) cout << x << "\n";

#else

#define LOG(x)

#endif

Display *display;
int xkbEventType;
Window root;
Atom netActiveWindow;

typedef int Layout;
const Layout Unknown = -1;

typedef map<Window, Layout> WindowLayouts;
WindowLayouts windowLayouts;

Layout getCurrentLayout() {
  XkbStateRec state;
  if (Success == XkbGetState(display, XkbUseCoreKbd, &state))
    return state.locked_group;
  return Unknown;
}

bool noWindow(Window window) {
  return window == None;
}

/*
 This justBelowRoot stuff is needed to cleanup windowLayouts map and prevent memory leak.
 It seems that event.xdestroywindow.window carries "just below root" window,
 though getActiveWindow return some its child.
 */

Window justBelowRoot(Window window) {
  LOG("justBelowRoot window=" << window);

  if (noWindow(window)) return window;

  for (;;) {
    unsigned numChildren;
    Window *children = 0, parent, rootWindow;
    int retCode = XQueryTree(display, window, &rootWindow, &parent, &children, &numChildren);

    LOG(
        "justBelowRoot XQueryTree retCode=" << retCode << " window=" << window << " parent=" << parent << " rootWindow=" << rootWindow << " children=" << children << " numChildren=" << numChildren);

    if (!retCode)
      return None;

    if (children)
      XFree(children);

    LOG("justBelowRoot XFree done");

    if (parent == rootWindow)
      break; window = parent; } return window;
}

Window getActiveWindow() {
  Window activeWindow = None;
  Atom actualType;
  int actualFormat;
  unsigned long nItems, bytesAfter;
  unsigned char *propReturn = 0;

  if (XGetWindowProperty(display, root, netActiveWindow, 0, sizeof(Window), 0, XA_WINDOW, &actualType, &actualFormat,
      &nItems, &bytesAfter, &propReturn) == Success && propReturn) {
    memcpy(&activeWindow, propReturn, sizeof(Window));
    XFree(propReturn);
    activeWindow = justBelowRoot(activeWindow);
  }
  return activeWindow;
}

void checkChangeLayout() {
  Window window = getActiveWindow();
  if (noWindow(window))
    return;

  Layout layout = getCurrentLayout();
  WindowLayouts::const_iterator i = windowLayouts.find(window);
  Layout saved = i != windowLayouts.end() ? i->second : 0;

  LOG("checkChangeLayout: window=" << window << " layout=" << layout << " saved=" << saved);

  if (layout != saved) {
    LOG("checkChangeLayout: change layout=" << saved);
    XkbLockGroup(display, XkbUseCoreKbd, saved);
  }
}

void proceedEvent(XEvent event) {
  LOG("event=" << event.type);

  if (event.type == PropertyNotify) {
    LOG("PropertyNotify window=" << event.xproperty.window);
    if (event.xproperty.window == root && event.xproperty.atom == netActiveWindow)
      checkChangeLayout();
  }

  else if (event.type == DestroyNotify) {
    LOG("DestroyNotify: window=" << event.xdestroywindow.window);
    windowLayouts.erase(event.xdestroywindow.window);
    checkChangeLayout();

  } else if (event.type == xkbEventType) {
    Window window = getActiveWindow();
    if (noWindow(window))
      return;

    Layout layout = getCurrentLayout();
    windowLayouts[window] = layout;
    LOG("xkbEventType: window=" << window << " layout=" << layout << " layouts=" << windowLayouts.size());

  } else
    LOG("proceedEvent type=" << event.type);

  LOG("event=" << event.type << " done.");
}

void mainLoop() {
  LOG("mainLoop");
  for (;;) {
    XEvent ev;
    XNextEvent(display, &ev);
    proceedEvent(ev);
  }
}

int errNoOp(Display *, XErrorEvent *) {
  return 0;
}

int main(int argc, char **argv) {
  string noDaemonArg = "-n", helpArg = "-h";
  if (argc > 2 || (argc == 2 && argv[1] != noDaemonArg) || (argc == 2 && argv[1] == helpArg)) {
    cout << "Keeps per window keyboard layout under X11\n\nUsage: perWindowLayout [ -n ] [ -h ]\n"
        "\t-n\tdo not daemonize\n"
        "\t-h\tthis message\n";
    return 1;
  }

  bool noDaemon = argc == 2 && argv[1] == noDaemonArg;
  if (!noDaemon && daemon(0, 0) != 0) {
    cerr << "Failed to daemonize\n";
    return 1;
  }

  int xkbError, reason;
  int major = XkbMajorVersion;
  int minor = XkbMinorVersion;
  display = XkbOpenDisplay(0, &xkbEventType, &xkbError, &major, &minor, &reason);
  if (!display) {
    cout << "Could not open display\n";
    return 1;
  }

  XSetErrorHandler(errNoOp);

  root = DefaultRootWindow(display);

  XWindowAttributes attributes;
  XGetWindowAttributes(display, root, &attributes);
  XSelectInput(display, root,
      StructureNotifyMask | SubstructureNotifyMask | PropertyChangeMask | KeymapStateMask | attributes.your_event_mask);

  netActiveWindow = XInternAtom(display, "_NET_ACTIVE_WINDOW", 0);

  XkbSelectEventDetails(display, XkbUseCoreKbd, XkbStateNotify, XkbAllStateComponentsMask, XkbGroupStateMask);

  LOG("init: major=" << major << " minor=" << minor << " xkbEventType=" << xkbEventType);

  mainLoop();
  return 0;
}
