/* Copyright (c) 2008 Stefan Endrullis, All Rights Reserved
 *
 * The contents of this file is dual-licensed under 2
 * alternative Open Source/Free licenses: LGPL 2.1 or later and
 * Apache License 2.0. (starting with JNA version 4.0.0).
 *
 * You can freely decide which license you want to apply to
 * the project.
 *
 * You may obtain a copy of the LGPL License at:
 *
 * http://www.gnu.org/licenses/licenses.html
 *
 * A copy is also included in the downloadable source code package
 * containing JNA, in file "LGPL2.1".
 *
 * You may obtain a copy of the Apache License at:
 *
 * http://www.apache.org/licenses/
 *
 * A copy is also included in the downloadable source code package
 * containing JNA, in file "AL2.0".
 */
package jnacontrib.x11.api;

import com.sun.jna.Native;
import com.sun.jna.NativeLong;
import com.sun.jna.Pointer;
import com.sun.jna.Memory;
import com.sun.jna.ptr.IntByReference;
import com.sun.jna.ptr.NativeLongByReference;
import com.sun.jna.ptr.PointerByReference;

import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.ArrayList;
import java.awt.Rectangle;

import com.sun.jna.platform.unix.X11;
import com.sun.jna.platform.unix.X11.Atom;
import com.sun.jna.platform.unix.X11.WindowByReference;

/**
 * Object oriented X window system.
 * <p/>
 * Some code segments in this class are based on the code of the program
 * wmctrl (licensed under GPLv2) written by Tomas Styblo &lt;tripie@cpan.org&gt;.
 * Thanks a lot, Tomas!
 *
 * @author Stefan Endrullis
 */
public class X {
    /** Remove/unset property. */
    public static final int _NET_WM_STATE_REMOVE = 0;
    /** Add/set property. */
    public static final int _NET_WM_STATE_ADD = 1;
    /** Toggle property. */
    public static final int _NET_WM_STATE_TOGGLE = 2;
    /** Maximal property value length. */
    public static final int MAX_PROPERTY_VALUE_LEN = 4096;

    private static final X11 x11 = X11.INSTANCE;

    private static int bytesToInt(byte[] prop) {
        return ((prop[3] & 0xff) << 24)
                | ((prop[2] & 0xff) << 16)
                | ((prop[1] & 0xff) << 8)
                | ((prop[0] & 0xff));
    }

    private static int bytesToInt(byte[] prop, int offset) {
        return ((prop[3 + offset] & 0xff) << 24)
                | ((prop[2 + offset] & 0xff) << 16)
                | ((prop[1 + offset] & 0xff) << 8)
                | ((prop[offset] & 0xff));
    }


    /**
     * X Display.
     */
    public static class Display {
        /**
         * Open display.
         */
        private X11.Display x11Display;
        /**
         * HashMap<String,X11.Atom>.
         */
        private HashMap<String, Atom> atomsHash = new HashMap<String, Atom>();

        /**
         * Creates the OOWindowUtils using the default display.
         */
        public Display() {
            x11Display = x11.XOpenDisplay(null);

            if (x11Display == null) {
                throw new Error("Can't open X Display");
            }
        }

        /**
         * Creates the OOWindowUtils using a given display.
         *
         * @param x11Display open display
         */
        public Display(X11.Display x11Display) {
            this.x11Display = x11Display;

            if (x11Display == null) {
                throw new Error("X Display is null");
            }
        }

        /**
         * Closes the display.
         */
        public void close() {
            x11.XCloseDisplay(x11Display);
        }

        /**
         * Flushes the output buffer / event queue.
         */
        public void flush() {
            x11.XFlush(x11Display);
        }

        /**
         * Returns the X11 display.
         *
         * @return X11 display
         */
        public X11.Display getX11Display() {
            return x11Display;
        }

        /**
         * Get internal atoms by name.
         *
         * @param name name of the atom
         * @return atom
         */
        public X11.Atom getAtom(String name) {
            X11.Atom atom = atomsHash.get(name);
            if (atom == null) {
                atom = x11.XInternAtom(x11Display, name, false);
                atomsHash.put(name, atom);
            }
            return atom;
        }

        /**
         * Returns the window manager information as an window.
         *
         * @return window manager information as an window
         * @throws X11Exception thrown if X11 window errors occurred
         */
        public Window getWindowManagerInfo() throws X11Exception {
            Window rootWindow = getRootWindow();

            try {
                return rootWindow.getWindowProperty(X11.XA_WINDOW, "_NET_SUPPORTING_WM_CHECK");
            } catch (X11Exception e) {
                try {
                    return rootWindow.getWindowProperty(X11.XA_CARDINAL, "_WIN_SUPPORTING_WM_CHECK");
                } catch (X11Exception e1) {
                    throw new X11Exception("Cannot get window manager info properties. (_NET_SUPPORTING_WM_CHECK or _WIN_SUPPORTING_WM_CHECK)");
                }
            }
        }

        /**
         * Returns the root window.
         *
         * @return root window
         */
        public Window getRootWindow() {
            return new Window(this, x11.XDefaultRootWindow(x11Display));
        }

        /**
         * Returns the current active window.
         *
         * @return current active window
         * @throws X11Exception thrown if X11 window errors occurred
         */
        public Window getActiveWindow() throws X11Exception {
            return getRootWindow().getWindowProperty(X11.XA_WINDOW, "_NET_ACTIVE_WINDOW");
        }

        /**
         * Returns all windows managed by the window manager.
         *
         * @return all windows managed by the window manager
         * @throws X11Exception thrown if X11 window errors occurred
         */
        public Window[] getWindows() throws X11Exception {
            byte[] bytes;
            Window rootWindow = getRootWindow();

            try {
                bytes = rootWindow.getProperty(X11.XA_WINDOW, "_NET_CLIENT_LIST");
            } catch (X11Exception e) {
                try {
                    bytes = rootWindow.getProperty(X11.XA_CARDINAL, "_WIN_CLIENT_LIST");
                } catch (X11Exception e1) {
                    throw new X11Exception("Cannot get client list properties (_NET_CLIENT_LIST or _WIN_CLIENT_LIST)");
                }
            }

            Window[] windowList = new Window[bytes.length / X11.Window.SIZE];

            for (int i = 0; i < windowList.length; i++) {
                windowList[i] = new Window(this, new X11.Window(bytesToInt(bytes, X11.XID.SIZE * i)));
            }

            return windowList;
        }

        /**
         * Returns the number of desktops.
         *
         * @return number of desktops
         * @throws X11Exception thrown if X11 window errors occurred
         */
        public int getDesktopCount() throws X11Exception {
            Window root = getRootWindow();

            try {
                return root.getIntProperty(X11.XA_CARDINAL, "_NET_NUMBER_OF_DESKTOPS");
            } catch (X11Exception e) {
                try {
                    return root.getIntProperty(X11.XA_CARDINAL, "_WIN_WORKSPACE_COUNT");
                } catch (X11Exception e1) {
                    throw new X11Exception("Cannot get number of desktops properties (_NET_NUMBER_OF_DESKTOPS or _WIN_WORKSPACE_COUNT)");
                }
            }
        }

        /**
         * Returns the number of the active desktop.
         *
         * @return number of the active desktop
         * @throws X11Exception thrown if X11 window errors occurred
         */
        public int getActiveDesktopNumber() throws X11Exception {
            Window root = getRootWindow();
            int cur_desktop;

            try {
                cur_desktop = root.getIntProperty(X11.XA_CARDINAL, "_NET_CURRENT_DESKTOP");
            } catch (X11Exception e) {
                try {
                    cur_desktop = root.getIntProperty(X11.XA_CARDINAL, "_WIN_WORKSPACE");
                } catch (X11Exception e1) {
                    throw new X11Exception("Cannot get current desktop properties (_NET_CURRENT_DESKTOP or _WIN_WORKSPACE property)");
                }
            }

            return cur_desktop;
        }

        /**
         * Returns the available desktops.
         *
         * @return available desktops
         * @throws X11Exception thrown if X11 window errors occurred
         */
        public Desktop[] getDesktops() throws X11Exception {
            Window root = getRootWindow();
            String[] desktopNames;
            try {
                desktopNames = root.getUtf8ListProperty(getAtom("UTF8_STRING"), "_NET_DESKTOP_NAMES");
            } catch (X11Exception e) {
                try {
                    desktopNames = root.getStringListProperty(X11.XA_STRING, "_WIN_WORKSPACE_NAMES");
                } catch (X11Exception e1) {
                    throw new X11Exception("Cannot get desktop names properties (_NET_DESKTOP_NAMES or _WIN_WORKSPACE_NAMES)");
                }
            }

            Desktop[] desktops = new Desktop[getDesktopCount()];
            for (int i = 0; i < desktops.length; i++) {
                desktops[i] = new Desktop(this, i, desktopNames[i]);
            }

            return desktops;
        }

        /**
         * Switches to the given desktop.
         *
         * @param nr desktop number
         * @throws X11Exception thrown if X11 window errors occurred
         */
        public void switchDesktop(int nr) throws X11Exception {
            getRootWindow().clientMsg("_NET_CURRENT_DESKTOP", nr, 0, 0, 0, 0);
        }

        /**
         * Sets the "showing the desktop" state.
         *
         * @param state true if the desktop should be shown
         * @throws X11Exception thrown if X11 window errors occurred
         */
        public void showingDesktop(boolean state) throws X11Exception {
            getRootWindow().clientMsg("_NET_SHOWING_DESKTOP", state ? 1 : 0, 0, 0, 0, 0);
        }

        /**
         * Enables / disables the auto-repeat of pressed keys.
         *
         * @param on true if auto-repeat shall be enabled
         */
        public void setKeyAutoRepeat(boolean on) {
            if (on) {
                x11.XAutoRepeatOn(x11Display);
            } else {
                x11.XAutoRepeatOff(x11Display);
            }
        }

        /**
         * Returns the key symbol corresponding to the the key name.
         *
         * @param keyName name of the key
         * @return key symbol
         */
        public X11.KeySym getKeySym(String keyName) {
            return x11.XStringToKeysym(keyName);
        }

        /**
         * Returns the key symbol corresponding to the keycode.
         *
         * @param keyCode keycode
         * @param index element of the keycode vector
         * @return key symbol
         */
        public X11.KeySym getKeySym(byte keyCode, int index) {
            return x11.XKeycodeToKeysym(x11Display, keyCode, index);
        }

        /**
         * Returns the keycode corresponding to the key symbol.
         *
         * @param keySym key symbol
         * @return keycode
         */
        public byte getKeyCode(X11.KeySym keySym) {
            return x11.XKeysymToKeycode(x11Display,  keySym);
        }

        /**
         * Returns the keycode corresponding to the key name.
         *
         * @param keyName name of the key
         * @return keycode
         */
        public byte getKeyCode(String keyName) {
            return x11.XKeysymToKeycode(x11Display,  getKeySym(keyName));
        }

        /**
         * Returns the key name corresponding to the key symbol.
         *
         * @param keySym key symbol
         * @return name of the key
         */
        public String getKeyName(X11.KeySym keySym) {
            return x11.XKeysymToString(keySym);
        }

        /**
         * Returns the key name corresponding to the keycode and the index in the keycode vector.
         *
         * @param keyCode keycode
         * @param index index in the keycode vector
         * @return name of the key
         */
        public String getKeyName(byte keyCode, int index) {
            return getKeyName(getKeySym(keyCode, index));
        }

        /**
         * Returns the modifier keymap.
         *
         * @return modifier keymap
         */
        public ModifierKeymap getModifierKeymap() {
            X11.XModifierKeymapRef xModifierKeymapRef = x11.XGetModifierMapping(x11Display);
            ModifierKeymap modifierKeymap = new ModifierKeymap(xModifierKeymapRef);
            x11.XFreeModifiermap(xModifierKeymapRef);
            return modifierKeymap;
        }

        /**
         * Sets the modifier keymap.
         *
         * @param modifierKeymap modifier keymap
         */
        public void setModifierKeymap(ModifierKeymap modifierKeymap) {
            X11.XModifierKeymapRef xModifierKeymapRef = modifierKeymap.toXModifierKeyamp();
            x11.XSetModifierMapping(x11Display,xModifierKeymapRef);
        }
    }


    /**
     * Modifier keymap. The lists shift, lock, control, mod1, mod1, mod1, mod1, mod1
     * contain the keycodes as Byte objects. You can directly access these lists to
     * read, replace, remove or insert new keycodes to these modifiers.
     * To apply a new modifier keymap call
     * {@link X.Display#setModifierKeymap(ModifierKeymap)}.
     */
    public static class ModifierKeymap {
        /** Shift modifier as an ArrayList&lt;Byte&gt;. */
        public ArrayList<Byte> shift = new ArrayList<Byte>(4);
        /** Lock modifier as an ArrayList&lt;Byte&gt;. */
        public ArrayList<Byte> lock = new ArrayList<Byte>(4);
        /** Control modifier as an ArrayList&lt;Byte&gt;. */
        public ArrayList<Byte> control = new ArrayList<Byte>(4);
        /** Mod1 modifier as an ArrayList&lt;Byte&gt;. */
        public ArrayList<Byte> mod1 = new ArrayList<Byte>(4);
        /** Mod2 modifier as an ArrayList&lt;Byte&gt;. */
        public ArrayList<Byte> mod2 = new ArrayList<Byte>(4);
        /** Mod3 modifier as an ArrayList&lt;Byte&gt;. */
        public ArrayList<Byte> mod3 = new ArrayList<Byte>(4);
        /** Mod4 modifier as an ArrayList&lt;Byte&gt;. */
        public ArrayList<Byte> mod4 = new ArrayList<Byte>(4);
        /** Mod5 modifier as an ArrayList&lt;Byte&gt;. */
        public ArrayList<Byte> mod5 = new ArrayList<Byte>(4);

        /**
         * Creates an empty modifier keymap.
         */
        public ModifierKeymap() {
        }

        /**
         * Creates a modifier keymap and reads the modifiers from the XModifierKeymap.
         *
         * @param xModifierKeymapRef XModifierKeymap
         */
        public ModifierKeymap(X11.XModifierKeymapRef xModifierKeymapRef) {
            fromXModifierKeymap(xModifierKeymapRef);
        }

        /**
         * Reads all modifiers from the XModifierKeymap.
         *
         * @param xModifierKeymapRef XModifierKeymap
         */
        public void fromXModifierKeymap(X11.XModifierKeymapRef xModifierKeymapRef) {
            int count = xModifierKeymapRef.max_keypermod;
            byte[] keys = xModifierKeymapRef.modifiermap.getByteArray(0, 8*count);

            ArrayList<Byte>[] allModifiers = getAllModifiers();

            for (int modNr = 0; modNr < 8; modNr++) {
                ArrayList<Byte> modifier = allModifiers[modNr];
                modifier.clear();

                for (int keyNr = 0; keyNr < count; keyNr++) {
                    byte key = keys[modNr*count + keyNr];
                    if (key != 0) {
                        modifier.add(Byte.valueOf(key));
                    }
                }
            }
        }

        /**
         * Returns an XModifierKeymap corresponding to this object.
         *
         * @return XModifierKeymap
         */
        public X11.XModifierKeymapRef toXModifierKeyamp() {
            ArrayList<Byte>[] allModifiers = getAllModifiers();

            // determine max list size
            int count = 0;
            for (int i = 0; i < allModifiers.length; i++) {
                count = Math.max(count, allModifiers[i].size());
            }

            byte[] keys = new byte[8*count];
            for (int modNr = 0; modNr < 8; modNr++) {
                ArrayList<Byte> modifier = allModifiers[modNr];

                for (int keyNr = 0; keyNr < modifier.size(); keyNr++) {
                    keys[modNr*count + keyNr] = modifier.get(keyNr).byteValue();
                }
            }

            X11.XModifierKeymapRef xModifierKeymapRef = new X11.XModifierKeymapRef();
            xModifierKeymapRef.max_keypermod = count;
            xModifierKeymapRef.modifiermap = new Memory(keys.length);
            xModifierKeymapRef.modifiermap.write(0, keys, 0, keys.length);

            return xModifierKeymapRef;
        }

        /**
         * Returns all modifiers as an array.
         *
         * @return array of modifier lists
         */
        public ArrayList<Byte>[] getAllModifiers() {
            return new ArrayList[]{
                shift, lock, control, mod1, mod2, mod3, mod4, mod5
            };
        }
    }

    /**
     * X Desktop.
     */
    public static class Desktop {
        public X.Display display;
        public int number;
        public String name;

        public Desktop(Display display, int number, String name) {
            this.display = display;
            this.number = number;
            this.name = name;
        }
    }


    /**
     * X Window.
     */
    public static class Window {
        private X.Display display;
        private X11.Window x11Window;

        /**
         * Returns the X11 window object.
         *
         * @return X11 window
         */
        public X11.Window getX11Window() {
            return x11Window;
        }

        /**
         * Returns the ID of the window.
         *
         * @return window ID
         */
        public int getID() {
            return x11Window.intValue();
        }

        /**
         * Creates the window.
         *
         * @param display   display where the window is allocated
         * @param x11Window X11 window
         */
        public Window(X.Display display, X11.Window x11Window) {
            this.display = display;
            this.x11Window = x11Window;
        }

        /**
         * Returns the title of the window.
         *
         * @return title of the window
         * @throws X11Exception thrown if X11 window errors occurred
         */
        public String getTitle() throws X11Exception {
            try {
                return getUtf8Property(display.getAtom("UTF8_STRING"), "_NET_WM_NAME");
            } catch (X11Exception e) {
                return getUtf8Property(X11.XA_STRING, X11.XA_WM_NAME);
            }

        }

        /**
         * Returns the window class.
         *
         * @return window class
         * @throws X11Exception thrown if X11 window errors occurred
         */
        public String getWindowClass() throws X11Exception {
            return getUtf8Property(X11.XA_STRING, X11.XA_WM_CLASS);
        }

        /**
         * Returns the PID of the window.
         *
         * @return PID of the window
         * @throws X11Exception thrown if X11 window errors occurred
         */
        public Integer getPID() throws X11Exception {
            return getIntProperty(X11.XA_CARDINAL, "_NET_WM_PID");
        }

        /**
         * Returns the desktop ID of the window.
         *
         * @return desktop ID of the window
         * @throws X11Exception thrown if X11 window errors occurred
         */
        public int getDesktop() throws X11Exception {
            try {
                return getIntProperty(X11.XA_CARDINAL, "_NET_WM_DESKTOP");
            } catch (X11Exception e) {
                return getIntProperty(X11.XA_CARDINAL, "_WIN_WORKSPACE");
            }
        }

        /**
         * Returns the client machine name of the window.
         *
         * @return client machine name of the window
         * @throws X11Exception thrown if X11 window errors occurred
         */
        public String getMachine() throws X11Exception {
            return getStringProperty(X11.XA_STRING, "WM_CLIENT_MACHINE");
        }

        /**
         * Returns the XWindowAttributes of the window.
         *
         * @return XWindowAttributes of the window
         */
        public X11.XWindowAttributes getXWindowAttributes() {
            X11.XWindowAttributes xwa = new X11.XWindowAttributes();
            x11.XGetWindowAttributes(display.x11Display, x11Window, xwa);

            return xwa;
        }

        /**
         * Returns the geometry of the window.
         *
         * @return geometry of the window
         */
        public Geometry getGeometry() {
            X11.WindowByReference junkRoot = new X11.WindowByReference();
            IntByReference junkX = new IntByReference();
            IntByReference junkY = new IntByReference();
            IntByReference x = new IntByReference();
            IntByReference y = new IntByReference();
            IntByReference width = new IntByReference();
            IntByReference height = new IntByReference();
            IntByReference borderWidth = new IntByReference();
            IntByReference depth = new IntByReference();

            x11.XGetGeometry(display.x11Display, x11Window, junkRoot, junkX, junkY, width, height, borderWidth, depth);

            x11.XTranslateCoordinates(display.x11Display, x11Window, junkRoot.getValue(), junkX.getValue(),
                    junkY.getValue(), x, y, junkRoot);

            return new Geometry(x.getValue(), y.getValue(), width.getValue(), height.getValue(),
                    borderWidth.getValue(), depth.getValue());
        }

        /**
         * Returns the bounding box of the window.
         *
         * @return bounding box of the window
         */
        public Rectangle getBounds() {
            X11.WindowByReference junkRoot = new X11.WindowByReference();
            IntByReference junkX = new IntByReference();
            IntByReference junkY = new IntByReference();
            IntByReference x = new IntByReference();
            IntByReference y = new IntByReference();
            IntByReference width = new IntByReference();
            IntByReference height = new IntByReference();
            IntByReference border_width = new IntByReference();
            IntByReference depth = new IntByReference();

            x11.XGetGeometry(display.x11Display, x11Window, junkRoot, junkX, junkY, width, height, border_width, depth);

            x11.XTranslateCoordinates(display.x11Display, x11Window, junkRoot.getValue(), junkX.getValue(),
                    junkY.getValue(), x, y, junkRoot);

            int xVal = x.getValue();
            int yVal = y.getValue();

            return new Rectangle(xVal, yVal, xVal + width.getValue(), yVal + height.getValue());
        }

        /**
         * Activates the window.
         *
         * @throws X11Exception thrown if X11 window errors occurred
         */
        public void activate() throws X11Exception {
            clientMsg("_NET_ACTIVE_WINDOW", 0, 0, 0, 0, 0);
            x11.XMapRaised(display.x11Display, x11Window);
        }

        /**
         * Moves the window to the specified desktop.
         *
         * @param desktopNr desktop
         * @return X11.SUCCESS if closing was successful
         * @throws X11Exception thrown if X11 window errors occurred
         */
        public int moveToDesktop(int desktopNr) throws X11Exception {
            return clientMsg("_NET_WM_DESKTOP", desktopNr, 0, 0, 0, 0);
        }

        /**
         * Selects the input events to listen for.
         *
         * @param eventMask event mask representing the events to listen for
         */
        public void selectInput(int eventMask) {
            x11.XSelectInput(display.x11Display, x11Window, new NativeLong(eventMask));
        }

        public int nextEvent(X11.XEvent event) {
            return x11.XNextEvent(display.x11Display, event);
        }

        public void sendEvent(int eventMask, X11.XEvent event) {
            x11.XSendEvent(display.x11Display, x11Window, 1, new NativeLong(eventMask), event);
        }

        /**
         * Closes the window gracefully.
         *
         * @return X11.SUCCESS if closing was successful
         * @throws X11Exception thrown if X11 window errors occurred
         */
        public int close() throws X11Exception {
            return clientMsg("_NET_CLOSE_WINDOW", 0, 0, 0, 0, 0);
        }

        /**
         * Returns the property value as integer.
         *
         * @param xa_prop_type property type
         * @param xa_prop_name property name
         * @return property value as integer or null if not found
         * @throws X11Exception thrown if X11 window errors occurred
         */
        public Integer getIntProperty(X11.Atom xa_prop_type, X11.Atom xa_prop_name) throws X11Exception {
            byte[] property = getProperty(xa_prop_type, xa_prop_name);
            if( property == null ){
                return null;
            }
            return bytesToInt(property);
        }

        /**
         * Returns the property value as integer.
         *
         * @param xa_prop_type property type
         * @param xa_prop_name property name
         * @return property value as integer
         * @throws X11Exception thrown if X11 window errors occurred
         */
        public Integer getIntProperty(X11.Atom xa_prop_type, String xa_prop_name) throws X11Exception {
            return getIntProperty(xa_prop_type, display.getAtom(xa_prop_name));
        }

        /**
         * Returns the property value as window.
         *
         * @param xa_prop_type property type
         * @param xa_prop_name property name
         * @return property value as window, or null if not found
         * @throws X11Exception thrown if X11 window errors occurred
         */
        public Window getWindowProperty(X11.Atom xa_prop_type, X11.Atom xa_prop_name) throws X11Exception {
            Integer windowId = getIntProperty(xa_prop_type, xa_prop_name);
            if( windowId == null ){
                return null;
            }
            X11.Window x11Window = new X11.Window(windowId);
            return new Window(display, x11Window);
        }

        /**
         * Returns the property value as window.
         *
         * @param xa_prop_type property type
         * @param xa_prop_name property name
         * @return property value as window
         * @throws X11Exception thrown if X11 window errors occurred
         */
        public Window getWindowProperty(X11.Atom xa_prop_type, String xa_prop_name) throws X11Exception {
            return getWindowProperty(xa_prop_type, display.getAtom(xa_prop_name));
        }

        /**
         * Returns the property value as a null terminated byte array.
         *
         * @param xa_prop_type property type
         * @param xa_prop_name property name
         * @return property value as a null terminated byte array, or null if not found
         * @throws X11Exception thrown if X11 window errors occurred
         */
        public byte[] getNullTerminatedProperty(X11.Atom xa_prop_type, X11.Atom xa_prop_name) throws X11Exception {
            byte[] bytesOrig = getProperty(xa_prop_type, xa_prop_name);
            byte[] bytesDest;

            if( bytesOrig == null ){
                return null;
            }
            // search for '\0'
            int i;
            for (i = 0; i < bytesOrig.length; i++) {
                if (bytesOrig[i] == '\0') break;
            }

            if (i < bytesOrig.length - 1) {
                bytesDest = new byte[i + 1];
                System.arraycopy(bytesOrig, 0, bytesDest, 0, i + 1);
            } else {
                bytesDest = bytesOrig;
            }

            return bytesDest;
        }

        /**
         * Returns the property value as a null terminated byte array.
         *
         * @param xa_prop_type property type
         * @param xa_prop_name property name
         * @return property value as a null terminated byte array
         * @throws X11Exception thrown if X11 window errors occurred
         */
        public byte[] getNullTerminatedProperty(X11.Atom xa_prop_type, String xa_prop_name) throws X11Exception {
            return getNullTerminatedProperty(xa_prop_type, display.getAtom(xa_prop_name));
        }

        /**
         * Returns the property value as byte array where every '\0' character is replaced by '.'.
         *
         * @param xa_prop_type property type
         * @param xa_prop_name property name
         * @return property value as byte array where every '\0' character is replaced by '.'.  null if the property was not found
         * @throws X11Exception thrown if X11 window errors occurred
         */
        public byte[] getNullReplacedStringProperty(X11.Atom xa_prop_type, X11.Atom xa_prop_name) throws X11Exception {
            byte[] bytes = getProperty(xa_prop_type, xa_prop_name);

            if( bytes == null ){
                return null;
            }

            // search for '\0'
            int i;
            for (i = 0; i < bytes.length; i++) {
                if (bytes[i] == '\0') {
                    bytes[i] = '.';
                }
            }

            return bytes;
        }

        /**
         * Returns the property value as byte array where every '\0' character is replaced by '.'.
         *
         * @param xa_prop_type property type
         * @param xa_prop_name property name
         * @return property value as byte array where every '\0' character is replaced by '.'
         * @throws X11Exception thrown if X11 window errors occurred
         */
        public byte[] getNullReplacedStringProperty(X11.Atom xa_prop_type, String xa_prop_name) throws X11Exception {
            return getNullReplacedStringProperty(xa_prop_type, display.getAtom(xa_prop_name));
        }

        /**
         * Returns the property value as string where every '\0' character is replaced by '.'.
         *
         * @param xa_prop_type property type
         * @param xa_prop_name property name
         * @return property value as string where every '\0' character is replaced by '.'
         * @throws X11Exception thrown if X11 window errors occurred
         */
        public String getStringProperty(X11.Atom xa_prop_type, X11.Atom xa_prop_name) throws X11Exception {
            return new String(getNullReplacedStringProperty(xa_prop_type, xa_prop_name));
        }

        /**
         * Returns the property value as string where every '\0' character is replaced by '.'.
         *
         * @param xa_prop_type property type
         * @param xa_prop_name property name
         * @return property value as string where every '\0' character is replaced by '.'
         * @throws X11Exception thrown if X11 window errors occurred
         */
        public String getStringProperty(X11.Atom xa_prop_type, String xa_prop_name) throws X11Exception {
            return new String(getNullReplacedStringProperty(xa_prop_type, xa_prop_name));
        }

        /**
         * Returns the property value as string list.
         *
         * @param xa_prop_type property type
         * @param xa_prop_name property name
         * @return property value as string list
         * @throws X11Exception thrown if X11 window errors occurred
         */
        public String[] getStringListProperty(X11.Atom xa_prop_type, X11.Atom xa_prop_name) throws X11Exception {
            return new String(getProperty(xa_prop_type, xa_prop_name)).split("\0");
        }

        /**
         * Returns the property value as string list.
         *
         * @param xa_prop_type property type
         * @param xa_prop_name property name
         * @return property value as string list, or null if the property value does not exist
         * @throws X11Exception thrown if X11 window errors occurred
         */
        public String[] getStringListProperty(X11.Atom xa_prop_type, String xa_prop_name) throws X11Exception {
            byte[] property = getProperty(xa_prop_type, xa_prop_name);
            if( property == null ){
                return null;
            }
            return new String(property).split("\0");
        }

        /**
         * Returns the property value as UTF8 string where every '\0' character is replaced by '.'.
         *
         * @param xa_prop_type property type
         * @param xa_prop_name property name
         * @return property value as UTF8 string where every '\0' character is replaced by '.'
         * @throws X11Exception thrown if X11 window errors occurred
         */
        public String getUtf8Property(X11.Atom xa_prop_type, X11.Atom xa_prop_name) throws X11Exception {
            try {
                byte[] property = getNullReplacedStringProperty(xa_prop_type, xa_prop_name);
                if( property == null ){
                    return null;
                }
                return new String(property, "UTF8");
            } catch (UnsupportedEncodingException e) {
                throw new X11Exception(e);
            }
        }

        /**
         * Returns the property value as UTF8 string where every '\0' character is replaced by '.'.
         *
         * @param xa_prop_type property type
         * @param xa_prop_name property name
         * @return property value as UTF8 string where every '\0' character is replaced by '.'
         * @throws X11Exception thrown if X11 window errors occurred
         */
        public String getUtf8Property(X11.Atom xa_prop_type, String xa_prop_name) throws X11Exception {
            return getUtf8Property(xa_prop_type, display.getAtom(xa_prop_name));
        }

        /**
         * Returns the property value as UTF8 string list
         *
         * @param xa_prop_type property type
         * @param xa_prop_name property name
         * @return property value as UTF8 string list
         * @throws X11Exception thrown if X11 window errors occurred
         */
        public String[] getUtf8ListProperty(X11.Atom xa_prop_type, X11.Atom xa_prop_name) throws X11Exception {
            try {
                return new String(getProperty(xa_prop_type, xa_prop_name), "UTF8").split("\0");
            } catch (UnsupportedEncodingException e) {
                throw new X11Exception(e);
            }
        }

        /**
         * Returns the property value as UTF8 string list
         *
         * @param xa_prop_type property type
         * @param xa_prop_name property name
         * @return property value as UTF8 string list
         * @throws X11Exception thrown if X11 window errors occurred
         */
        public String[] getUtf8ListProperty(X11.Atom xa_prop_type, String xa_prop_name) throws X11Exception {
            return getUtf8ListProperty(xa_prop_type, display.getAtom(xa_prop_name));
        }

        /**
         * Returns the property value as a byte array.
         *
         * @param xa_prop_type property type
         * @param xa_prop_name property name
         * @return property value as a byte array
         * @throws X11Exception thrown if X11 window errors occurred
         */
        public byte[] getProperty(X11.Atom xa_prop_type, X11.Atom xa_prop_name) throws X11Exception {
            X11.AtomByReference xa_ret_type_ref = new X11.AtomByReference();
            IntByReference ret_format_ref = new IntByReference();
            NativeLongByReference ret_nitems_ref = new NativeLongByReference();
            NativeLongByReference ret_bytes_after_ref = new NativeLongByReference();
            PointerByReference ret_prop_ref = new PointerByReference();

            NativeLong long_offset = new NativeLong(0);
            NativeLong long_length = new NativeLong(MAX_PROPERTY_VALUE_LEN / 4);

            /* MAX_PROPERTY_VALUE_LEN / 4 explanation (XGetWindowProperty manpage):
             *
             * long_length = Specifies the length in 32-bit multiples of the
             *               data to be retrieved.
             */
            if (x11.XGetWindowProperty(display.x11Display, x11Window, xa_prop_name, long_offset, long_length, false,
                    xa_prop_type, xa_ret_type_ref, ret_format_ref,
                    ret_nitems_ref, ret_bytes_after_ref, ret_prop_ref) != X11.Success) {
                String prop_name = x11.XGetAtomName(display.x11Display, xa_prop_name);
                throw new X11Exception("Cannot get " + prop_name + " property.");
            }

            X11.Atom xa_ret_type = xa_ret_type_ref.getValue();
            Pointer ret_prop = ret_prop_ref.getValue();

            if (xa_ret_type == null) {
                //the specified property does not exist for the specified window
                return null;
            }

            if( xa_ret_type == null ){
                //the specified property does not exist for the specified window
                return null;
            }

            if( xa_ret_type == null ){
                //the specified property does not exist for the specified window
                return null;
            }

            if (xa_ret_type == null || xa_prop_type == null ||
                    !xa_ret_type.toNative().equals(xa_prop_type.toNative())) {
                x11.XFree(ret_prop);
                String prop_name = x11.XGetAtomName(display.x11Display, xa_prop_name);
                throw new X11Exception("Invalid type of " + prop_name + " property");
            }

            int ret_format = ret_format_ref.getValue();
            long ret_nitems = ret_nitems_ref.getValue().longValue();

            // null terminate the result to make string handling easier
            int nbytes;
            if (ret_format == 32)
                nbytes = Native.LONG_SIZE;
            else if (ret_format == 16)
                nbytes = Native.LONG_SIZE / 2;
            else if (ret_format == 8)
                nbytes = 1;
            else if (ret_format == 0)
                nbytes = 0;
            else
                throw new X11Exception("Invalid return format");
            int length = Math.min((int) ret_nitems * nbytes, MAX_PROPERTY_VALUE_LEN);

            byte[] ret = ret_prop.getByteArray(0, length);

            x11.XFree(ret_prop);
            return ret;
        }

        /**
         * Returns the property value as a byte array.
         *
         * @param xa_prop_type property type
         * @param xa_prop_name property name
         * @return property value as a byte array
         * @throws X11Exception thrown if X11 window errors occurred
         */
        public byte[] getProperty(X11.Atom xa_prop_type, String xa_prop_name) throws X11Exception {
            return getProperty(xa_prop_type, display.getAtom(xa_prop_name));
        }

        public int clientMsg(String msg, int data0, int data1, int data2, int data3, int data4) throws X11Exception {
            return clientMsg(
                    msg,
                    new NativeLong(data0),
                    new NativeLong(data1),
                    new NativeLong(data2),
                    new NativeLong(data3),
                    new NativeLong(data4)
                    );
        }

        public int clientMsg(String msg, NativeLong data0, NativeLong data1, NativeLong data2, NativeLong data3, NativeLong data4) throws X11Exception {
            X11.XClientMessageEvent event;
            NativeLong mask = new NativeLong(X11.SubstructureRedirectMask | X11.SubstructureNotifyMask);

            event = new X11.XClientMessageEvent();
            event.type = X11.ClientMessage;
            event.serial = new NativeLong(0);
            event.send_event = 1;
            event.message_type = display.getAtom(msg);
            event.window = x11Window;
            event.format = 32;
            event.data.setType(NativeLong[].class);
            event.data.l[0] = data0;
            event.data.l[1] = data1;
            event.data.l[2] = data2;
            event.data.l[3] = data3;
            event.data.l[4] = data4;

            X11.XEvent e = new X11.XEvent();
            e.setTypedValue(event);

            if (x11.XSendEvent(display.x11Display, display.getRootWindow().x11Window, 0, mask, e) != 0) {
                return X11.Success;
            } else {
                throw new X11Exception("Cannot send " + msg + " event.");
            }
        }

        public Window[] getSubwindows() throws X11Exception {
            WindowByReference root = new WindowByReference();
            WindowByReference parent = new WindowByReference();
            PointerByReference children = new PointerByReference();
            IntByReference childCount = new IntByReference();

            if (x11.XQueryTree(display.x11Display, x11Window, root, parent, children, childCount) == 0){
                throw new X11Exception("Can't query subwindows");
            }

            if( childCount.getValue() == 0 ){
                return null;
            }

            Window[] retVal = new Window[ childCount.getValue() ];
            //Depending on if we're running on 64-bit or 32-bit systems,
            //the Window ID size may be different; we need to make sure that
            //we get the data properly no matter what
            if (X11.XID.SIZE == 4) {
                int[] windows = children.getValue().getIntArray( 0, childCount.getValue() );
                for( int x = 0; x < retVal.length; x++ ){
                    X11.Window win = new X11.Window( windows [ x ] );
                    retVal[ x ] = new Window( display, win );
                }
            }
            else {
                long[] windows = children.getValue().getLongArray( 0, childCount.getValue() );
                for( int x = 0; x < retVal.length; x++ ){
                    X11.Window win = new X11.Window( windows [ x ] );
                    retVal[ x ] = new Window( display, win );
                }
            }
            x11.XFree(children.getValue());

            return retVal;
        }


        public String toString() {
            return x11Window.toString();
        }

        public static class Geometry {
            public int x, y, width, height, borderWidth, depth;

            public Geometry(int x, int y, int width, int height, int border_width, int depth) {
                this.x = x;
                this.y = y;
                this.width = width;
                this.height = height;
                this.borderWidth = border_width;
                this.depth = depth;
            }
        }
    }

    /**
     * General exception which is thrown when an X11 window error occurred.
     */
    public static class X11Exception extends Exception {
        private static final long serialVersionUID = 1L;
        public X11Exception() {
        }

        public X11Exception(String message) {
            super(message);
        }

        public X11Exception(String message, Throwable cause) {
            super(message, cause);
        }

        public X11Exception(Throwable cause) {
            super(cause);
        }
    }
}
