/*
 * Copyright (c) 2008 Sun Microsystems, Inc. All Rights Reserved.
 * Copyright (c) 2010 JogAmp Community. All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 * 
 * - Redistribution of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 * 
 * - Redistribution in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 * 
 * Neither the name of Sun Microsystems, Inc. or the names of
 * contributors may be used to endorse or promote products derived from
 * this software without specific prior written permission.
 * 
 * This software is provided "AS IS," without a warranty of any kind. ALL
 * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES,
 * INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A
 * PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN
 * MICROSYSTEMS, INC. ("SUN") AND ITS LICENSORS SHALL NOT BE LIABLE FOR
 * ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR
 * DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES. IN NO EVENT WILL SUN OR
 * ITS LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR
 * DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE
 * DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY,
 * ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE, EVEN IF
 * SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
 */

package jogamp.nativewindow.x11;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import javax.media.nativewindow.AbstractGraphicsDevice;
import javax.media.nativewindow.NativeWindowException;
import javax.media.nativewindow.NativeWindowFactory;

import jogamp.nativewindow.Debug;
import jogamp.nativewindow.NWJNILibLoader;
import jogamp.nativewindow.ToolkitProperties;

import com.jogamp.common.util.LongObjectHashMap;
import com.jogamp.nativewindow.x11.X11GraphicsDevice;

/**
 * Contains a thread safe X11 utility to retrieve display connections.
 */
public class X11Util implements ToolkitProperties {
    /** 
     * See Bug 515 - https://jogamp.org/bugzilla/show_bug.cgi?id=515
     * <p> 
     * It is observed that ATI X11 drivers, eg.
     * <ul> 
     *   <li>fglrx 8.78.6,</li>
     *   <li>fglrx 11.08/8.881 and </li>
     *   <li>fglrx 11.11/8.911</li>
     * </ul>  
     * are quite sensitive to multiple Display connections.
     * </p>
     * <p>
     * With the above drivers closing displays shall happen in the same order as
     * they were opened, <b>or</b> shall not be closed at all!
     * If closed, some driver related bug appears and brings down the JVM.
     * </p>
     * <p>
     * You may test this, ie just reverse the destroy order below.
     * See also native test: jogl/test-native/displayMultiple02.c
     * </p>
     * <p>
     * Workaround is to not close them at all if driver vendor is ATI
     * during operation.
     * </p>
     * <p>
     * With ATI X11 drivers all connections must be closed at JVM shutdown,
     * otherwise a SIGSEGV (after JVM safepoint) will be caused. 
     * </p>
     */
    public static final boolean ATI_HAS_XCLOSEDISPLAY_BUG = !Debug.isPropertyDefined("nativewindow.debug.X11Util.ATI_HAS_NO_XCLOSEDISPLAY_BUG", true);

    /**
     * See Bug 623 - https://jogamp.org/bugzilla/show_bug.cgi?id=623
     */
    public static final boolean ATI_HAS_MULTITHREADING_BUG = !Debug.isPropertyDefined("nativewindow.debug.X11Util.ATI_HAS_NO_MULTITHREADING_BUG", true);

    public static final boolean DEBUG = Debug.debug("X11Util");
    public static final boolean XSYNC_ENABLED = Debug.isPropertyDefined("nativewindow.debug.X11Util.XSync", true);
    public static final boolean XERROR_STACKDUMP = DEBUG || Debug.isPropertyDefined("nativewindow.debug.X11Util.XErrorStackDump", true);
    private static final boolean TRACE_DISPLAY_LIFECYCLE = Debug.isPropertyDefined("nativewindow.debug.X11Util.TraceDisplayLifecycle", true);
    private static String nullDisplayName = null;
    private static volatile boolean isInit = false;
    private static boolean markAllDisplaysUnclosable = false; // ATI/AMD X11 driver issues
    private static boolean hasThreadingIssues = false; // ATI/AMD X11 driver issues

    private static Object setX11ErrorHandlerLock = new Object();
    private static final String X11_EXTENSION_ATIFGLRXDRI     = "ATIFGLRXDRI";
    private static final String X11_EXTENSION_ATIFGLEXTENSION = "ATIFGLEXTENSION";
    
    /**
     * Called by {@link NativeWindowFactory#initSingleton()}
     * @see ToolkitProperties
     */
    public static void initSingleton() {
        if(!isInit) {
            synchronized(X11Util.class) {
                if(!isInit) {
                    isInit = true;
                    if(DEBUG) {
                        System.out.println("X11Util.initSingleton()");
                    }
                    if(!NWJNILibLoader.loadNativeWindow("x11")) {
                        throw new NativeWindowException("NativeWindow X11 native library load error.");
                    }
        
                    final boolean isInitOK = initialize0( XERROR_STACKDUMP );
        
                    final boolean hasX11_EXTENSION_ATIFGLRXDRI, hasX11_EXTENSION_ATIFGLEXTENSION;
                    final long dpy = X11Lib.XOpenDisplay(null);
                    if(0 != dpy) {
                        if(XSYNC_ENABLED) {
                            X11Lib.XSynchronize(dpy, true);
                        }                    
                        try {
                            nullDisplayName = X11Lib.XDisplayString(dpy);
                            hasX11_EXTENSION_ATIFGLRXDRI = X11Lib.QueryExtension(dpy, X11_EXTENSION_ATIFGLRXDRI);
                            hasX11_EXTENSION_ATIFGLEXTENSION = X11Lib.QueryExtension(dpy, X11_EXTENSION_ATIFGLEXTENSION);
                        } finally {
                            X11Lib.XCloseDisplay(dpy);
                        }
                    } else {
                        nullDisplayName = "nil";
                        hasX11_EXTENSION_ATIFGLRXDRI = false;
                        hasX11_EXTENSION_ATIFGLEXTENSION = false;
                    }
                    hasThreadingIssues = ATI_HAS_MULTITHREADING_BUG && ( hasX11_EXTENSION_ATIFGLRXDRI || hasX11_EXTENSION_ATIFGLEXTENSION );
                    markAllDisplaysUnclosable = ATI_HAS_XCLOSEDISPLAY_BUG && ( hasX11_EXTENSION_ATIFGLRXDRI || hasX11_EXTENSION_ATIFGLEXTENSION );
                    
                    if(DEBUG) {
                        System.err.println("X11Util.initSingleton(): OK "+isInitOK+"]"+
                                           ",\n\t X11 Display(NULL) <"+nullDisplayName+">"+
                                           ",\n\t XSynchronize Enabled: " + XSYNC_ENABLED+
                                           ",\n\t X11_EXTENSION_ATIFGLRXDRI " + hasX11_EXTENSION_ATIFGLRXDRI+
                                           ",\n\t X11_EXTENSION_ATIFGLEXTENSION " + hasX11_EXTENSION_ATIFGLEXTENSION+
                                           ",\n\t requiresToolkitLock "+requiresToolkitLock()+
                                           ",\n\t hasThreadingIssues "+hasThreadingIssues()+
                                           ",\n\t markAllDisplaysUnclosable "+getMarkAllDisplaysUnclosable()
                                           );
                        // Thread.dumpStack();
                    }
                }
            }
        }
    }
    
    // not exactly thread safe, but good enough for our purpose,
    // which is to tag a NamedDisplay uncloseable after creation.
    private static Object globalLock = new Object(); 
    private static LongObjectHashMap openDisplayMap = new LongObjectHashMap(); // handle -> name
    private static List<NamedDisplay> openDisplayList = new ArrayList<NamedDisplay>();     // open, no close attempt
    private static List<NamedDisplay> reusableDisplayList = new ArrayList<NamedDisplay>(); // close attempt, marked uncloseable, for reuse
    private static List<NamedDisplay> pendingDisplayList = new ArrayList<NamedDisplay>();  // all open (close attempt or reusable) in creation order
    private static final HashMap<String /* displayName */, Boolean> displayXineramaEnabledMap = new HashMap<String, Boolean>();
    
    /** 
     * Cleanup resources.
     * <p>
     * Called by {@link NativeWindowFactory#shutdown()}
     * </p>
     * @see ToolkitProperties
     */
    public static void shutdown() {
        if(isInit) {
            synchronized(X11Util.class) {
                if(isInit) {                    
                    final boolean isJVMShuttingDown = NativeWindowFactory.isJVMShuttingDown() ;
                    if(DEBUG || openDisplayMap.size() > 0 || reusableDisplayList.size() > 0 || pendingDisplayList.size() > 0) {
                        System.err.println("X11Util.Display: Shutdown (JVM shutdown: "+isJVMShuttingDown+
                                           ", open (no close attempt): "+openDisplayMap.size()+"/"+openDisplayList.size()+
                                           ", reusable (open, marked uncloseable): "+reusableDisplayList.size()+
                                           ", pending (open in creation order): "+pendingDisplayList.size()+
                                           ")");
                        if(DEBUG) {
                            Thread.dumpStack();
                        }
                        if( openDisplayList.size() > 0) {
                            X11Util.dumpOpenDisplayConnections();
                        }
                        if(DEBUG) {
                            if( reusableDisplayList.size() > 0 || pendingDisplayList.size() > 0 ) {
                                X11Util.dumpPendingDisplayConnections();
                            }
                        }
                    }
            
                    synchronized(globalLock) {
                        // Only at JVM shutdown time, since AWT impl. seems to 
                        // dislike closing of X11 Display's (w/ ATI driver). 
                        if( isJVMShuttingDown ) {
                            isInit = false;                            
                            closePendingDisplayConnections();    
                            openDisplayList.clear();
                            reusableDisplayList.clear();
                            pendingDisplayList.clear();
                            openDisplayMap.clear();
                            displayXineramaEnabledMap.clear();
                            shutdown0();
                        }
                    }
                }
            }
        }
    }
    
    /**
     * Called by {@link NativeWindowFactory#initSingleton()}
     * @see ToolkitProperties
     */
    public static final boolean requiresToolkitLock() {
        return true; // JAWT locking: yes, instead of native X11 locking w use a recursive lock per display connection.
    }
    
    /**
     * Called by {@link NativeWindowFactory#initSingleton()}
     * @see ToolkitProperties
     */
    public static final boolean hasThreadingIssues() {
        return hasThreadingIssues; // JOGL impl. may utilize special locking "somewhere" 
    }
    
    public static void setX11ErrorHandler(boolean onoff, boolean quiet) {
        synchronized(setX11ErrorHandlerLock) {
            setX11ErrorHandler0(onoff, quiet);
        }
    }

    public static String getNullDisplayName() {
        return nullDisplayName;
    }
    
    public static boolean getMarkAllDisplaysUnclosable() {
        return markAllDisplaysUnclosable;
    }
    
    private X11Util() {}

    public static class NamedDisplay {
        final String name;
        final long   handle;
        final int    hash32;
        int    refCount;
        boolean unCloseable;
        Throwable creationStack;

        protected NamedDisplay(String name, long handle) {
            this.name=name;
            this.handle=handle;
            this.refCount=0;
            this.unCloseable=false;
            {
                int h32;
                h32 = 31 +                 (int)   handle          ; // lo
                h32 = ((h32 << 5) - h32) + (int) ( handle >>> 32 ) ; // hi
                hash32 = h32;
            }
            if(DEBUG) {
                this.creationStack=new Throwable("NamedDisplay Created at:");
            } else {
                this.creationStack=null;
            }
        }

        public final int hashCode() {
            return hash32;
        }
        
        public final boolean equals(Object obj) {
            if(this == obj) { return true; }
            if(obj instanceof NamedDisplay) {
                return handle == ((NamedDisplay) obj).handle;
            }
            return false;
        }
        
        public final void addRef() { refCount++; }
        public final void removeRef() { refCount--; }
        
        public final String getName() { return name; }
        public final long   getHandle() { return handle; }
        public final int    getRefCount() { return refCount; }

        public final void setUncloseable(boolean v) { unCloseable = v; }
        public final boolean isUncloseable() { return unCloseable; }
        public final Throwable getCreationStack() { return creationStack; }

        @Override
        public Object clone() throws CloneNotSupportedException {
          return super.clone();
        }

        @Override
        public String toString() {
            return "NamedX11Display["+name+", 0x"+Long.toHexString(handle)+", refCount "+refCount+", unCloseable "+unCloseable+"]";
        }
    }

    /**
     * Closing pending Display connections in original creation order, if {@link #getMarkAllDisplaysUnclosable()} is true.
     *
     * @return number of closed Display connections
     */
    private static int closePendingDisplayConnections() {
        int num=0;
        synchronized(globalLock) {
            if( getMarkAllDisplaysUnclosable() ) {
                for(int i=0; i<pendingDisplayList.size(); i++) {
                    final NamedDisplay ndpy = (NamedDisplay) pendingDisplayList.get(i);
                    if(DEBUG) {
                        final boolean closeAttempted = !openDisplayMap.containsKey(ndpy.getHandle());
                        System.err.println("X11Util.closePendingDisplayConnections(): Closing ["+i+"]: "+ndpy+" - closeAttempted "+closeAttempted);
                    }
                    XCloseDisplay(ndpy.getHandle());
                    num++;
                }
                System.err.println("X11Util.closePendingDisplayConnections(): Closed "+num+" pending display connections");
            }
        }
        return num;
    }

    public static int getOpenDisplayConnectionNumber() {
        synchronized(globalLock) {
            return openDisplayList.size();
        }
    }
        
    public static void dumpOpenDisplayConnections() {
        synchronized(globalLock) {
            System.err.println("X11Util: Open X11 Display Connections: "+openDisplayList.size());
            for(int i=0; i<openDisplayList.size(); i++) {
                NamedDisplay ndpy = openDisplayList.get(i);
                System.err.println("X11Util: Open["+i+"]: "+ndpy);
                if(null!=ndpy) {
                    Throwable t = ndpy.getCreationStack();
                    if(null!=t) {
                        t.printStackTrace();
                    }
                }
            }
        }
    }
    
    public static int getReusableDisplayConnectionNumber() {
        synchronized(globalLock) {
            return reusableDisplayList.size();
        }
    }

    public static int getPendingDisplayConnectionNumber() {
        synchronized(globalLock) {
            return pendingDisplayList.size();
        }
    }

    public static void dumpPendingDisplayConnections() {
        synchronized(globalLock) {
            System.err.println("X11Util: Reusable X11 Display Connections: "+reusableDisplayList.size());
            for(int i=0; i<reusableDisplayList.size(); i++) {
                NamedDisplay ndpy = (NamedDisplay) reusableDisplayList.get(i);
                System.err.println("X11Util: Reusable["+i+"]: "+ndpy);
                if(null!=ndpy) {
                    Throwable t = ndpy.getCreationStack();
                    if(null!=t) {
                        t.printStackTrace();
                    }
                }
            }
            System.err.println("X11Util: Pending X11 Display Connections (creation order): "+pendingDisplayList.size());
            for(int i=0; i<pendingDisplayList.size(); i++) {
                NamedDisplay ndpy = (NamedDisplay) pendingDisplayList.get(i);
                System.err.println("X11Util: Pending["+i+"]: "+ndpy);
                if(null!=ndpy) {
                    Throwable t = ndpy.getCreationStack();
                    if(null!=t) {
                        t.printStackTrace();
                    }
                }
            }
        }
    }

    public static boolean markDisplayUncloseable(long handle) {
        NamedDisplay ndpy;
        synchronized(globalLock) {
            ndpy = (NamedDisplay) openDisplayMap.get(handle);
        }
        if( null != ndpy ) {
            ndpy.setUncloseable(true);
            return true;
        }
        return false;
    }

    /** Returns a created or reused named display. */
    public static long openDisplay(String name) {
        long dpy = 0;
        NamedDisplay namedDpy = null;
        name = validateDisplayName(name);
        boolean reused = false;
        
        synchronized(globalLock) {
            for(int i=0; i<reusableDisplayList.size(); i++) {
                if(reusableDisplayList.get(i).getName().equals(name)) {
                    namedDpy = reusableDisplayList.remove(i);
                    dpy = namedDpy.getHandle();
                    reused = true;
                    break;
                }
            }
            if(0 == dpy) {
                dpy = XOpenDisplay(name);
                if(0==dpy) {
                    throw new NativeWindowException("X11Util.Display: Unable to create a display("+name+") connection. Thread "+Thread.currentThread().getName());
                }
                // if you like to debug and synchronize X11 commands ..
                // setSynchronizeDisplay(dpy, true);
                namedDpy = new NamedDisplay(name, dpy);
                pendingDisplayList.add(namedDpy);
            }
            namedDpy.addRef();
            openDisplayMap.put(dpy, namedDpy);
            openDisplayList.add(namedDpy);
            if(markAllDisplaysUnclosable) {
                namedDpy.setUncloseable(true);
            }
        }
        if(DEBUG) {
            System.err.println("X11Util.Display: openDisplay [reuse "+reused+"] "+namedDpy+". Thread "+Thread.currentThread().getName());
            // Thread.dumpStack();
        }
        return namedDpy.getHandle();
    }

    public static void closeDisplay(long handle) {
        NamedDisplay namedDpy;

        synchronized(globalLock) {
            namedDpy = (NamedDisplay) openDisplayMap.remove(handle);
            if(null==namedDpy) {
                X11Util.dumpPendingDisplayConnections();
                throw new RuntimeException("X11Util.Display: Display(0x"+Long.toHexString(handle)+") with given handle is not mapped. Thread "+Thread.currentThread().getName());
            }
            if(namedDpy.getHandle()!=handle) {
                X11Util.dumpPendingDisplayConnections();
                throw new RuntimeException("X11Util.Display: Display(0x"+Long.toHexString(handle)+") Mapping error: "+namedDpy+". Thread "+Thread.currentThread().getName());
            }
    
            namedDpy.removeRef();
            if(!openDisplayList.remove(namedDpy)) { throw new RuntimeException("Internal: "+namedDpy); }
            
            if(!namedDpy.isUncloseable()) {
                XCloseDisplay(namedDpy.getHandle());
                pendingDisplayList.remove(namedDpy);
            } else {
                // for reuse
                X11Lib.XSync(namedDpy.getHandle(), true); // flush output buffer and discard all events                
                reusableDisplayList.add(namedDpy);
            }
            
            if(DEBUG) {
                System.err.println("X11Util.Display: Closed (real: "+(!namedDpy.isUncloseable())+") "+namedDpy+". Thread "+Thread.currentThread().getName());
            }    
        }
    }

    public static NamedDisplay getNamedDisplay(long handle) {
        synchronized(globalLock) {
            return (NamedDisplay) openDisplayMap.get(handle);
        }
    }

    /** 
     * @return If name is null, it returns the previous queried NULL display name,
     * otherwise the name. */
    public static String validateDisplayName(String name) {
        return ( null == name || AbstractGraphicsDevice.DEFAULT_CONNECTION.equals(name) ) ? getNullDisplayName() : name ;
    }

    public static String validateDisplayName(String name, long handle) {
        if( ( null==name || AbstractGraphicsDevice.DEFAULT_CONNECTION.equals(name) ) && 0!=handle) {
            name = X11Lib.XDisplayString(handle);
        }
        return validateDisplayName(name);
    }

    /*******************************
     **
     ** Locked X11Lib wrapped functions
     **
     *******************************/

    public static long XOpenDisplay(String arg0) {
        long handle = X11Lib.XOpenDisplay(arg0);
        if(XSYNC_ENABLED && 0 != handle) {
            X11Lib.XSynchronize(handle, true);
        }                    
        if(TRACE_DISPLAY_LIFECYCLE) {
            System.err.println(Thread.currentThread()+" - X11Util.XOpenDisplay("+arg0+") 0x"+Long.toHexString(handle));
            // Thread.dumpStack();
        }
        return handle;
    }

    public static int XCloseDisplay(long display) {
        if(TRACE_DISPLAY_LIFECYCLE) {
            System.err.println(Thread.currentThread()+" - X11Util.XCloseDisplay() 0x"+Long.toHexString(display));
            // Thread.dumpStack();
        }
        int res = -1;
        try {
            res = X11Lib.XCloseDisplay(display);
        } catch (Exception ex) {
            System.err.println("X11Util: Catched Exception:");
            ex.printStackTrace();
        }
        return res;            
    }

    static volatile boolean XineramaFetched = false;
    static long XineramaLibHandle = 0;
    static long XineramaQueryFunc = 0;
    
    public static boolean XineramaIsEnabled(X11GraphicsDevice device) {
        if(null == device) {
            throw new IllegalArgumentException("X11 Display device is NULL");
        }
        device.lock();
        try {
            return XineramaIsEnabled(device.getHandle());
        } finally {
            device.unlock();
        }        
    }
    
    public static boolean XineramaIsEnabled(long displayHandle) {
        if( 0 == displayHandle ) {
            throw new IllegalArgumentException("X11 Display handle is NULL");
        }
        final String displayName = X11Lib.XDisplayString(displayHandle);
        synchronized(displayXineramaEnabledMap) {
            final Boolean b = displayXineramaEnabledMap.get(displayName);
            if(null != b) {
                return b.booleanValue();
            }
        }
        final boolean res;
        if(!XineramaFetched) { // volatile: ok
            synchronized(X11Util.class) {
                if( !XineramaFetched ) {
                    XineramaLibHandle = X11Lib.XineramaGetLibHandle();
                    if(0 != XineramaLibHandle) {
                        XineramaQueryFunc = X11Lib.XineramaGetQueryFunc(XineramaLibHandle);
                    }
                    XineramaFetched = true;
                }
            }
        }
        if(0!=XineramaQueryFunc) {
            res = X11Lib.XineramaIsEnabled(XineramaQueryFunc, displayHandle);
        } else {
            if(DEBUG) {
                System.err.println("XineramaIsEnabled: Couldn't bind to Xinerama - lib 0x"+Long.toHexString(XineramaLibHandle)+
                                   "query 0x"+Long.toHexString(XineramaQueryFunc));
            }
            res = false;
        }
        synchronized(displayXineramaEnabledMap) {
            if(DEBUG) {
                System.err.println("XineramaIsEnabled Cache: Display "+displayName+" (0x"+Long.toHexString(displayHandle)+") -> "+res);
            }
            displayXineramaEnabledMap.put(displayName, Boolean.valueOf(res));
        }
        return res;
    }
    
    private static final String getCurrentThreadName() { return Thread.currentThread().getName(); } // Callback for JNI
    private static final void dumpStack() { Thread.dumpStack(); } // Callback for JNI
    
    private static native boolean initialize0(boolean debug);
    private static native void shutdown0();
    private static native void setX11ErrorHandler0(boolean onoff, boolean quiet);
}
