/*
 * Copyright (c) 2008-2009 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 javax.media.nativewindow;

import com.jogamp.common.util.ReflectionUtil;
import jogamp.nativewindow.Debug;
import jogamp.nativewindow.DefaultGraphicsConfigurationFactoryImpl;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Provides the mechanism by which the graphics configuration for a
 * window can be chosen before the window is created. The graphics
 * configuration decides parameters related to hardware accelerated rendering such
 * as the OpenGL pixel format. <br>
 * On some window systems (EGL/OpenKODE and X11 in particular) it is necessary to
 * choose the graphics configuration early at window creation time. <br>
 * Note that the selection of the graphics configuration is an algorithm which does not have
 * strong dependencies on the particular Java window toolkit in use
 * (e.g., AWT) and therefore it is strongly desirable to factor this
 * functionality out of the core {@link NativeWindowFactory} so that
 * new window toolkits can replace just the {@link
 * NativeWindowFactory} and reuse the graphics configuration selection
 * algorithm provided by, for example, an OpenGL binding.
 */

public abstract class GraphicsConfigurationFactory {
    protected static final boolean DEBUG;

    private static class DeviceCapsType {
        public final Class<?> deviceType;
        public final Class<?> capsType;
        private final int hash32;

        public DeviceCapsType(final Class<?> deviceType, final Class<?> capsType) {
            this.deviceType = deviceType;
            this.capsType = capsType;

            // 31 * x == (x << 5) - x
            int hash32 = 31 + deviceType.hashCode();
            hash32 = ((hash32 << 5) - hash32) + capsType.hashCode();
            this.hash32 = hash32;
        }

        @Override
        public final int hashCode() {
            return hash32;
        }

        @Override
        public final boolean equals(final Object obj) {
            if(this == obj)  { return true; }
            if (obj instanceof DeviceCapsType) {
                final DeviceCapsType dct = (DeviceCapsType)obj;
                return deviceType == dct.deviceType && capsType == dct.capsType;
            }
            return false;
        }

        @Override
        public final String toString() {
            return "DeviceCapsType["+deviceType.getName()+", "+capsType.getName()+"]";
        }

    }

    private static final Map<DeviceCapsType, GraphicsConfigurationFactory> registeredFactories;
    private static final DeviceCapsType defaultDeviceCapsType;
    static boolean initialized = false;

    static {
        DEBUG = Debug.debug("GraphicsConfiguration");
        if(DEBUG) {
            System.err.println(Thread.currentThread().getName()+" - Info: GraphicsConfigurationFactory.<init>");
            // Thread.dumpStack();
        }
        registeredFactories = Collections.synchronizedMap(new HashMap<DeviceCapsType, GraphicsConfigurationFactory>());
        defaultDeviceCapsType = new DeviceCapsType(AbstractGraphicsDevice.class, CapabilitiesImmutable.class);
    }

    public static synchronized void initSingleton() {
        if(!initialized) {
            initialized = true;

            if(DEBUG) {
                System.err.println(Thread.currentThread().getName()+" - GraphicsConfigurationFactory.initSingleton()");
            }

            // Register the default no-op factory for arbitrary
            // AbstractGraphicsDevice implementations, including
            // AWTGraphicsDevice instances -- the OpenGL binding will take
            // care of handling AWTGraphicsDevices on X11 platforms (as
            // well as X11GraphicsDevices in non-AWT situations)
            registerFactory(defaultDeviceCapsType.deviceType, defaultDeviceCapsType.capsType, new DefaultGraphicsConfigurationFactoryImpl());

            if (NativeWindowFactory.TYPE_X11 == NativeWindowFactory.getNativeWindowType(true)) {
                try {
                    ReflectionUtil.callStaticMethod("jogamp.nativewindow.x11.X11GraphicsConfigurationFactory",
                                                    "registerFactory", null, null, GraphicsConfigurationFactory.class.getClassLoader());
                } catch (final Exception e) {
                    throw new RuntimeException(e);
                }
                if(NativeWindowFactory.isAWTAvailable()) {
                    try {
                        ReflectionUtil.callStaticMethod("jogamp.nativewindow.x11.awt.X11AWTGraphicsConfigurationFactory",
                                                        "registerFactory", null, null, GraphicsConfigurationFactory.class.getClassLoader());
                    } catch (final Exception e) { /* n/a */ }
                }
            }
        }
    }

    public static synchronized void shutdown() {
        if(initialized) {
            initialized = false;
            if(DEBUG) {
                System.err.println(Thread.currentThread().getName()+" - GraphicsConfigurationFactory.shutdown()");
            }
            registeredFactories.clear();
        }
    }

    protected static String getThreadName() {
        return Thread.currentThread().getName();
    }

    protected static String toHexString(final int val) {
        return "0x" + Integer.toHexString(val);
    }

    protected static String toHexString(final long val) {
        return "0x" + Long.toHexString(val);
    }

    /** Creates a new NativeWindowFactory instance. End users do not
        need to call this method. */
    protected GraphicsConfigurationFactory() {
    }

    /**
     * Returns the graphics configuration factory for use with the
     * given device and capability.
     *
     * @see #getFactory(Class, Class)
     */
    public static GraphicsConfigurationFactory getFactory(final AbstractGraphicsDevice device, final CapabilitiesImmutable caps) {
        if (device == null) {
            throw new IllegalArgumentException("null device");
        }
        if (caps == null) {
            throw new IllegalArgumentException("null caps");
        }
        return getFactory(device.getClass(), caps.getClass());
    }

    /**
     * Returns the graphics configuration factory for use with the
     * given device and capability class.
     * <p>
     * Note: Registered device types maybe classes or interfaces, where capabilities types are interfaces only.
     * </p>
     *
     * <p>
     * Pseudo code for finding a suitable factory is:
     * <pre>
        For-All devT := getTopDownDeviceTypes(deviceType)
            For-All capsT := getTopDownCapabilitiesTypes(capabilitiesType)
               f = factory.get(devT, capsT);
               if(f) { return f; }
            end
        end
     * </pre>
     * </p>
     *
     * @param deviceType the minimum capabilities class type accepted, must implement or extend {@link AbstractGraphicsDevice}
     * @param capabilitiesType the minimum capabilities class type accepted, must implement or extend {@link CapabilitiesImmutable}
     *
     * @throws IllegalArgumentException if the deviceType does not implement {@link AbstractGraphicsDevice} or
     *                                  capabilitiesType does not implement {@link CapabilitiesImmutable}
     */
    public static GraphicsConfigurationFactory getFactory(final Class<?> deviceType, final Class<?> capabilitiesType)
        throws IllegalArgumentException, NativeWindowException
    {
        if (!(defaultDeviceCapsType.deviceType.isAssignableFrom(deviceType))) {
            throw new IllegalArgumentException("Given class must implement AbstractGraphicsDevice");
        }
        if (!(defaultDeviceCapsType.capsType.isAssignableFrom(capabilitiesType))) {
            throw new IllegalArgumentException("Given capabilities class must implement CapabilitiesImmutable");
        }
        if(DEBUG) {
            Thread.dumpStack();
            System.err.println("GraphicsConfigurationFactory.getFactory: "+deviceType.getName()+", "+capabilitiesType.getName());
            dumpFactories();
        }

        final List<Class<?>> deviceTypes = getAllAssignableClassesFrom(defaultDeviceCapsType.deviceType, deviceType, false);
        if(DEBUG) {
            System.err.println("GraphicsConfigurationFactory.getFactory() deviceTypes: " + deviceTypes);
        }
        final List<Class<?>> capabilitiesTypes = getAllAssignableClassesFrom(defaultDeviceCapsType.capsType, capabilitiesType, true);
        if(DEBUG) {
            System.err.println("GraphicsConfigurationFactory.getFactory() capabilitiesTypes: " + capabilitiesTypes);
        }
        for(int j=0; j<deviceTypes.size(); j++) {
            final Class<?> interfaceDevice = deviceTypes.get(j);
            for(int i=0; i<capabilitiesTypes.size(); i++) {
                final Class<?> interfaceCaps = capabilitiesTypes.get(i);
                final DeviceCapsType dct = new DeviceCapsType(interfaceDevice, interfaceCaps);
                final GraphicsConfigurationFactory factory = registeredFactories.get(dct);
                if (factory != null) {
                    if(DEBUG) {
                        System.err.println("GraphicsConfigurationFactory.getFactory() found "+dct+" -> "+factory);
                    }
                    return factory;
                }
            }
        }
        // Return the default
        final GraphicsConfigurationFactory factory = registeredFactories.get(defaultDeviceCapsType);
        if(DEBUG) {
            System.err.println("GraphicsConfigurationFactory.getFactory() DEFAULT "+defaultDeviceCapsType+" -> "+factory);
        }
        return factory;
    }
    private static ArrayList<Class<?>> getAllAssignableClassesFrom(final Class<?> superClassOrInterface, final Class<?> fromClass, final boolean interfacesOnly) {
        // Using a todo list avoiding a recursive loop!
        final ArrayList<Class<?>> inspectClasses  = new ArrayList<Class<?>>();
        final ArrayList<Class<?>> resolvedInterfaces = new ArrayList<Class<?>>();
        inspectClasses.add(fromClass);
        for(int j=0; j<inspectClasses.size(); j++) {
            final Class<?> clazz = inspectClasses.get(j);
            getAllAssignableClassesFrom(superClassOrInterface, clazz, interfacesOnly, resolvedInterfaces, inspectClasses);
        }
        return resolvedInterfaces;
    }
    private static void getAllAssignableClassesFrom(final Class<?> superClassOrInterface, final Class<?> fromClass, final boolean interfacesOnly, final List<Class<?>> resolvedInterfaces, final List<Class<?>> inspectClasses) {
        final ArrayList<Class<?>> types = new ArrayList<Class<?>>();
        if( superClassOrInterface.isAssignableFrom(fromClass) && !resolvedInterfaces.contains(fromClass)) {
            if( !interfacesOnly || fromClass.isInterface() ) {
                types.add(fromClass);
            }
        }
        types.addAll(Arrays.asList(fromClass.getInterfaces()));

        for(int i=0; i<types.size(); i++) {
            final Class<?> iface = types.get(i);
            if( superClassOrInterface.isAssignableFrom(iface) && !resolvedInterfaces.contains(iface) ) {
                resolvedInterfaces.add(iface);
                if( !superClassOrInterface.equals(iface) && !inspectClasses.contains(iface) ) {
                    inspectClasses.add(iface); // safe add to todo list, avoiding a recursive nature
                }
            }
        }
        final Class<?> parentClass = fromClass.getSuperclass();
        if( null != parentClass && superClassOrInterface.isAssignableFrom(parentClass) && !inspectClasses.contains(parentClass) ) {
            inspectClasses.add(parentClass); // safe add to todo list, avoiding a recursive nature
        }
    }
    private static void dumpFactories() {
        final Set<DeviceCapsType> dcts = registeredFactories.keySet();
        int i=0;
        for(final Iterator<DeviceCapsType> iter = dcts.iterator(); iter.hasNext(); ) {
            final DeviceCapsType dct = iter.next();
            System.err.println("Factory #"+i+": "+dct+" -> "+registeredFactories.get(dct));
            i++;
        }
    }

    /**
     * Registers a GraphicsConfigurationFactory handling
     * the given graphics device and capability class.
     * <p>
     * This does not need to be called by end users, only implementors of new
     * GraphicsConfigurationFactory subclasses.
     * </p>
     *
     * <p>
     * Note: Registered device types maybe classes or interfaces, where capabilities types are interfaces only.
     * </p>
     *
     * <p>See {@link #getFactory(Class, Class)} for a description of the find algorithm.</p>
     *
     * @param deviceType the minimum capabilities class type accepted, must implement or extend interface {@link AbstractGraphicsDevice}
     * @param capabilitiesType the minimum capabilities class type accepted, must extend interface {@link CapabilitiesImmutable}
     * @return the previous registered factory, or null if none
     * @throws IllegalArgumentException if the given class does not implement AbstractGraphicsDevice
     */
    protected static GraphicsConfigurationFactory registerFactory(final Class<?> abstractGraphicsDeviceImplementor, final Class<?> capabilitiesType, final GraphicsConfigurationFactory factory)
        throws IllegalArgumentException
    {
        if (!(defaultDeviceCapsType.deviceType.isAssignableFrom(abstractGraphicsDeviceImplementor))) {
            throw new IllegalArgumentException("Given device class must implement AbstractGraphicsDevice");
        }
        if (!(defaultDeviceCapsType.capsType.isAssignableFrom(capabilitiesType))) {
            throw new IllegalArgumentException("Given capabilities class must implement CapabilitiesImmutable");
        }
        final DeviceCapsType dct = new DeviceCapsType(abstractGraphicsDeviceImplementor, capabilitiesType);
        final GraphicsConfigurationFactory prevFactory;
        if(null == factory) {
            prevFactory = registeredFactories.remove(dct);
            if(DEBUG) {
                System.err.println("GraphicsConfigurationFactory.registerFactory() remove "+dct+
                                   ", deleting: "+prevFactory);
            }
        } else {
            prevFactory = registeredFactories.put(dct, factory);
            if(DEBUG) {
                System.err.println("GraphicsConfigurationFactory.registerFactory() put "+dct+" -> "+factory+
                                   ", overridding: "+prevFactory);
            }
        }
        return prevFactory;
    }

    /**
     * <P> Selects a graphics configuration on the specified graphics
     * device compatible with the supplied {@link Capabilities}. Some
     * platforms (e.g.: X11, EGL, KD) require the graphics configuration
     * to be specified when the native window is created.
     * These architectures have seperated their device, screen, window and drawable
     * context and hence are capable of quering the capabilities for each screen.
     * A fully established window is not required.</P>
     *
     * <P>Other platforms (e.g. Windows, MacOSX) don't offer the mentioned seperation
     * and hence need a fully established window and it's drawable.
     * Here the validation of the capabilities is performed later.
     * In this case, the AbstractGraphicsConfiguration implementation
     * must allow an overwrite of the Capabilites, for example
     * {@link DefaultGraphicsConfiguration#setChosenCapabilities DefaultGraphicsConfiguration.setChosenCapabilities(..)}.
     * </P>
     *
     * <P>
     * This method is mainly intended to be both used and implemented by the
     * OpenGL binding.</P>
     *
     * <P> The concrete data type of the passed graphics device and
     * returned graphics configuration must be specified in the
     * documentation binding this particular API to the underlying
     * window toolkit. The Reference Implementation accepts {@link
     * com.jogamp.nativewindow.awt.AWTGraphicsDevice AWTGraphicsDevice} objects and returns {@link
     * com.jogamp.nativewindow.awt.AWTGraphicsConfiguration AWTGraphicsConfiguration} objects. On
     * X11 platforms where the AWT is not in use, it also accepts
     * {@link com.jogamp.nativewindow.x11.X11GraphicsDevice
     * X11GraphicsDevice} objects and returns {@link
     * com.jogamp.nativewindow.x11.X11GraphicsConfiguration
     * X11GraphicsConfiguration} objects.</P>
     *
     * @param capsChosen     the intermediate chosen capabilities to be refined by this implementation, may be equal to capsRequested
     * @param capsRequested  the original requested capabilities
     * @param chooser        the choosing implementation
     * @param screen         the referring Screen
     * @param nativeVisualID if not {@link VisualIDHolder#VID_UNDEFINED} it reflects a pre-chosen visualID of the native platform's windowing system.
     * @return               the complete GraphicsConfiguration
     *
     * @throws IllegalArgumentException if the data type of the passed
     *         AbstractGraphicsDevice is not supported by this
     *         NativeWindowFactory.
     * @throws NativeWindowException if any window system-specific errors caused
     *         the selection of the graphics configuration to fail.
     *
     * @see javax.media.nativewindow.GraphicsConfigurationFactory#chooseGraphicsConfiguration(Capabilities, CapabilitiesChooser, AbstractGraphicsScreen)
     * @see javax.media.nativewindow.DefaultGraphicsConfiguration#setChosenCapabilities(Capabilities caps)
     */
    public final AbstractGraphicsConfiguration
        chooseGraphicsConfiguration(final CapabilitiesImmutable capsChosen, final CapabilitiesImmutable capsRequested,
                                    final CapabilitiesChooser chooser,
                                    final AbstractGraphicsScreen screen, final int nativeVisualID)
        throws IllegalArgumentException, NativeWindowException {
        if(null==capsChosen) {
            throw new NativeWindowException("Chosen Capabilities are null");
        }
        if(null==capsRequested) {
            throw new NativeWindowException("Requested Capabilities are null");
        }
        if(null==screen) {
            throw new NativeWindowException("Screen is null");
        }
        final AbstractGraphicsDevice device =  screen.getDevice();
        if(null==device) {
            throw new NativeWindowException("Screen's Device is null");
        }
        device.lock();
        try {
            return chooseGraphicsConfigurationImpl(capsChosen, capsRequested, chooser, screen, nativeVisualID);
        } finally {
            device.unlock();
        }
    }

    protected abstract AbstractGraphicsConfiguration
        chooseGraphicsConfigurationImpl(CapabilitiesImmutable capsChosen, CapabilitiesImmutable capsRequested,
                                        CapabilitiesChooser chooser, AbstractGraphicsScreen screen, int nativeVisualID)
        throws IllegalArgumentException, NativeWindowException;

}
