/**
 * Copyright 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:
 *
 *    1. Redistributions of source code must retain the above copyright notice, this list of
 *       conditions and the following disclaimer.
 *
 *    2. Redistributions 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.
 *
 * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``AS IS'' AND ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JogAmp Community OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * The views and conclusions contained in the software and documentation are those of the
 * authors and should not be interpreted as representing official policies, either expressed
 * or implied, of JogAmp Community.
 */
package com.jogamp.newt;

import com.jogamp.newt.event.MonitorModeListener;

import jogamp.newt.Debug;

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import com.jogamp.nativewindow.AbstractGraphicsScreen;
import com.jogamp.nativewindow.NativeWindowException;
import com.jogamp.nativewindow.util.Rectangle;
import com.jogamp.nativewindow.util.RectangleImmutable;

/**
 * A screen may span multiple {@link MonitorDevice}s representing their combined virtual size.
 * <p>
 * All values of this interface are represented in pixel units, if not stated otherwise.
 * </p>
 *
 * <a name="coordinateSystem"><h5>Coordinate System</h5></a>
 * <p>
 *  <ul>
 *      <li>Screen space has it's origin in the top-left corner, and may not be at 0/0.</li>
 *      <li>{@link #getViewport() Virtual viewport} covers all {@link MonitorDevice}s {@link MonitorDevice#getViewport() viewports} and has it's origin in the top-left corner, and may not be at 0/0.</li>
 *  </ul>
 * </p>
*/
public abstract class Screen {

    /**
     * A 10s timeout for screen mode change. It is observed, that some platforms
     * need a notable amount of time for this task, especially in case of rotation change.
     */
    public static final int SCREEN_MODE_CHANGE_TIMEOUT = 10000;

    public static final boolean DEBUG = Debug.debug("Screen");

    /** return precomputed hashCode from FQN {@link #getFQName()} */
    @Override
    public abstract int hashCode();

    /** return true if obj is of type Display and both FQN {@link #getFQName()} equals */
    @Override
    public boolean equals(final Object obj) {
        if (this == obj) { return true; }
        if (obj instanceof Screen) {
            final Screen s = (Screen)obj;
            return s.getFQName().equals(getFQName());
        }
        return false;
    }

    /**
     * Manual trigger the native creation, if not done yet..<br>
     * This is useful to be able to request the {@link com.jogamp.nativewindow.AbstractGraphicsScreen}, via
     * {@link #getGraphicsScreen()}.<br>
     * Otherwise the abstract device won't be available before the dependent component (Window) is realized.
     * <p>
     * This method is usually invoke by {@link #addReference()}
     * </p>
     * <p>
     * This method invokes {@link Display#addReference()} after creating the native peer,<br>
     * which will issue {@link Display#createNative()} if the reference count was 0.
     * </p>
     * @throws NativeWindowException if the native creation failed.
     */
    public abstract void createNative() throws NativeWindowException;

    /**
     * Manually trigger the destruction, incl. native destruction.<br>
     * <p>
     * This method is usually invoke by {@link #removeReference()}
     * </p>
     * <p>
     * This method invokes {@link Display#removeReference()} after it's own destruction,<br>
     * which will issue {@link Display#destroy()} if the reference count becomes 0.
     * </p>
     */
    public abstract void destroy();

    public abstract boolean isNativeValid();

    /**
     * @return number of references
     */
    public abstract int getReferenceCount();

    /**
     * See {@link Display#addReference()}
     *
     * @return number of references post operation
     * @throws NativeWindowException if the native creation failed.
     * @see #removeReference()
     * @see #setDestroyWhenUnused(boolean)
     * @see #getDestroyWhenUnused()
     */
    public abstract int addReference() throws NativeWindowException;

    /**
     * See {@link Display#removeReference()}
     *
     * @return number of references post operation
     * @see #addReference()
     * @see #setDestroyWhenUnused(boolean)
     * @see #getDestroyWhenUnused()
     */
    public abstract int removeReference();

    public abstract AbstractGraphicsScreen getGraphicsScreen();

    /**
     * @return this Screen index of all Screens of {@link #getDisplay()}.
     */
    public abstract int getIndex();

    /**
     * See <a href="#coordinateSystem"> Coordinate System</a>.
     *
     * @return the x position of the virtual viewport's top-left origin in pixel units.
     */
    public abstract int getX();

    /**
     * See <a href="#coordinateSystem"> Coordinate System</a>.
     *
     * @return the y position of the virtual viewport's top-left origin in pixel units.
     */
    public abstract int getY();

    /**
     * @return the <b>rotated</b> virtual viewport's width in pixel units.
     */
    public abstract int getWidth();

    /**
     * @return the <b>rotated</b> virtual viewport's height in pixel units.
     */
    public abstract int getHeight();

    /**
     * See <a href="#coordinateSystem"> Coordinate System</a>.
     *
     * @return the <b>rotated</b> virtual viewport, i.e. top-left origin and size, in pixel units.
     * @see #getViewportInWindowUnits()
     */
    public abstract RectangleImmutable getViewport();

    /**
     * See <a href="#coordinateSystem"> Coordinate System</a>.
     *
     * @return the <b>rotated</b> virtual viewport, i.e. top-left origin and size, in window units.
     * @see #getViewport()
     */
    public abstract RectangleImmutable getViewportInWindowUnits();

    /**
     * @return the associated Display
     */
    public abstract Display getDisplay();

    /**
     * @return The screen fully qualified Screen name,
     * which is a key of {@link com.jogamp.newt.Display#getFQName()} + {@link #getIndex()}.
     */
    public abstract String getFQName();

    /**
     * Return a list of all {@link MonitorMode}s for all {@link MonitorDevice}s.
     * <p>
     * The list is ordered in descending order,
     * see {@link MonitorMode#compareTo(MonitorMode)}.
     * </p>
     */
    public abstract List<MonitorMode> getMonitorModes();

    /**
     * Return a list of available {@link MonitorDevice}s.
     */
    public abstract List<MonitorDevice> getMonitorDevices();

    /**
     * Returns the windowing manager's primary {@link MonitorDevice},
     * which holds the system menu bar, etc.
     * @see MonitorDevice#isPrimary()
     */
    public abstract MonitorDevice getPrimaryMonitor();

    /**
     * Returns the {@link MonitorDevice} with the highest {@link MonitorDevice#getViewportInWindowUnits() viewport}
     * {@link RectangleImmutable#coverage(RectangleImmutable) coverage} of the given rectangle in window units,
     * which is not a {@link MonitorDevice#isClone() clone}.
     * <p>
     * If no coverage is detected the first {@link MonitorDevice} is returned.
     * </p>
     * @param r arbitrary rectangle in window units
     */
    public final MonitorDevice getMainMonitor(final RectangleImmutable r) {
        MonitorDevice res = null;
        float maxCoverage = Float.MIN_VALUE;
        final List<MonitorDevice> monitors = getMonitorDevices();
        for(final MonitorDevice monitor : monitors) {
            if( !monitor.isClone() ) {
                final float coverage = monitor.getViewportInWindowUnits().coverage(r);
                if( coverage > maxCoverage ) {
                    maxCoverage = coverage;
                    res = monitor;
                }
            }
        }
        if( maxCoverage > 0.0f && null != res ) {
            return res;
        }
        return monitors.get(0);
    }

    /**
     * Returns the {@link MonitorDevice} which completely which {@link MonitorDevice#getViewportInWindowUnits() viewport}
     * completely {@link RectangleImmutable#contains(RectangleImmutable) coverage} the given rectangle in window units,
     * which is not a {@link MonitorDevice#isClone() clone}.
     * <p>
     * If no match is found, null is being returned
     * </p>
     * @param r arbitrary rectangle in window units
     */
    public final MonitorDevice getFullyEnteredMonitor(final RectangleImmutable r) {
        for(final MonitorDevice monitor : getMonitorDevices()) {
            if( !monitor.isClone() && monitor.getViewportInWindowUnits().contains(r) ) {
                return monitor;
            }
        }
        return null;
    }

    /**
     * Returns the {@link MonitorDevice} which matches the given integer monitorId.
     * <p>
     * If no match is found, null is being returned
     * </p>
     */
    public final MonitorDevice getMonitorById(final int monitorId) {
        for(final MonitorDevice monitor : getMonitorDevices()) {
            if( monitor.getId() == monitorId ) {
                return monitor;
            }
        }
        return null;
    }

    /**
     * Returns the {@link MonitorDevice} which matches the given long monitorHandle.
     * <p>
     * If no match is found, null is being returned
     * </p>
     */
    public final MonitorDevice getMonitorByHandle(final long monitorHandle) {
        for(final MonitorDevice monitor : getMonitorDevices()) {
            if( monitor.getHandle() == monitorHandle ) {
                return monitor;
            }
        }
        return null;
    }

    /**
     * Returns the {@link MonitorDevice} which matches the given name.
     * <p>
     * If no match is found or the given name is null or empty, null is being returned
     * </p>
     */
    public final MonitorDevice getMonitorByName(final String name) {
        if( null == name || name.isEmpty() ) {
            return null;
        }
        for(final MonitorDevice monitor : getMonitorDevices()) {
            if( name.equals( monitor.getName() ) ) {
                return monitor;
            }
        }
        return null;
    }

    /**
     * Calculates the union of all monitor's {@link MonitorDevice#getViewport() viewport} in pixel- and window units.
     * <p>
     * Should be equal to {@link #getX()}, {@link #getY()}, {@link #getWidth()} and {@link #getHeight()},
     * however, some native toolkits may choose a different virtual screen area.
     * </p>
     * @param viewport storage for result in pixel units, maybe null
     * @param viewportInWindowUnits storage for result in window units, maybe null
     */
    public final void unionOfMonitorViewports(final Rectangle viewport, final Rectangle viewportInWindowUnits) {
        MonitorDevice.unionOfViewports(viewport, viewportInWindowUnits, getMonitorDevices());
    }

    /**
     * @param sml {@link MonitorModeListener} to be added for {@link MonitorEvent}
     */
    public abstract void addMonitorModeListener(MonitorModeListener sml);

    /**
     * @param sml {@link MonitorModeListener} to be removed from {@link MonitorEvent}
     */
    public abstract void removeMonitorModeListener(MonitorModeListener sml);

    // Global Screens
    protected static final ArrayList<WeakReference<Screen>> screenList = new ArrayList<WeakReference<Screen>>();
    protected static int screensActive = 0;

    /**
     *
     * @param type
     * @param name
     * @param fromIndex start index, then increasing until found or end of list     *
     * @return
     */
    public static Screen getFirstScreenOf(final Display display, final int idx, final int fromIndex) {
        return getScreenOfImpl(display, idx, fromIndex, 1);
    }

    /**
     *
     * @param type
     * @param name
     * @param fromIndex start index, then decreasing until found or end of list. -1 is interpreted as size - 1.
     * @return
     */
    public static Screen getLastScreenOf(final Display display, final int idx, final int fromIndex) {
        return getScreenOfImpl(display, idx, fromIndex, -1);
    }

    private static Screen getScreenOfImpl(final Display display, final int idx, final int fromIndex, final int incr) {
        synchronized(screenList) {
            int i = fromIndex >= 0 ? fromIndex : screenList.size() - 1 ;
            while( ( incr > 0 ) ? i < screenList.size() : i >= 0 ) {
                final Screen screen = screenList.get(i).get();
                if( null == screen ) {
                    // Clear GC'ed dead reference entry!
                    screenList.remove(i);
                    if( incr < 0 ) {
                        // decrease
                        i+=incr;
                    } // else nop - remove shifted subsequent elements to the left
                } else {
                    if( screen.getDisplay().equals(display) &&
                        screen.getIndex() == idx ) {
                        return screen;
                    }
                    i+=incr;
                }
            }
        }
        return null;
    }

    protected static void addScreen2List(final Screen screen) {
        synchronized(screenList) {
            // GC before add
            int i=0;
            while( i < screenList.size() ) {
                if( null == screenList.get(i).get() ) {
                    screenList.remove(i);
                } else {
                    i++;
                }
            }
            screenList.add(new WeakReference<Screen>(screen));
        }
    }

    /** Returns the global screen collection */
    public static Collection<Screen> getAllScreens() {
        ArrayList<Screen> list;
        synchronized(screenList) {
            list = new ArrayList<Screen>();
            int i = 0;
            while( i < screenList.size() ) {
                final Screen s = screenList.get(i).get();
                if( null == s ) {
                    screenList.remove(i);
                } else {
                    list.add( screenList.get(i).get() );
                    i++;
                }
            }
        }
        return list;
    }

    public static int getActiveScreenNumber() {
        synchronized(screenList) {
            return screensActive;
        }
    }
}
