/*
 * Copyright (c) 2010, Oracle and/or its affiliates. All rights reserved.
 */
package com.sun.glass.ui;

import com.sun.glass.events.KeyEvent;
import com.sun.glass.ui.CommonDialogs.ExtensionFilter; 
import com.sun.glass.utils.Disposer;

import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Vector;

public abstract class Application extends ApplicationBase {
    public static class EventHandler {
        // currently used only on Mac OS X
        public void handleWillFinishLaunchingAction(Application app, long time) {
        }
        // currently used only on Mac OS X
        public void handleDidFinishLaunchingAction(Application app, long time) {
        }
        // currently used only on Mac OS X
        public void handleWillBecomeActiveAction(Application app, long time) {
        }
        // currently used only on Mac OS X
        public void handleDidBecomeActiveAction(Application app, long time) {
        }
        // currently used only on Mac OS X
        public void handleWillResignActiveAction(Application app, long time) {
        }
        // currently used only on Mac OS X
        public void handleDidResignActiveAction(Application app, long time) {
        }
        // currently used only on Mac OS X
        public void handleWillHideAction(Application app, long time) {
        }
        // currently used only on Mac OS X
        public void handleDidHideAction(Application app, long time) {
        }
        // currently used only on Mac OS X
        public void handleWillUnhideAction(Application app, long time) {
        }
        // currently used only on Mac OS X
        public void handleDidUnhideAction(Application app, long time) {
        }
        // currently used only on Mac OS X
        // the open files which started up the app will arrive before app becomes active
        public void handleOpenFilesAction(Application app, long time, String files[]) {
        }
        // currently used only on Mac OS X
        public void handleQuitAction(Application app, long time) {
    }
    }

    private EventHandler eventHandler;
    private boolean initialActiveEventReceived = false;
    private String initialOpenedFiles[] = null;

    private static boolean loaded = false;
    private static Application application;
    private Thread eventThread;
    
    public static synchronized void loadNativeLibrary(final String libname) {
        // load the native library of the specified libname.
        // the platform default by convention is "glass", all others should have a suffix, ie glass-x11
        if (loaded == false) {
            AccessController.doPrivileged(new PrivilegedAction() {
                public Object run() {
                    com.sun.glass.utils.NativeLibLoader.loadLibrary(libname);
                    return null;
                }
            });
            loaded = true;
        }
    }

    public static synchronized void loadNativeLibrary() {
        // use the "platform default" name of "glass" 
        loadNativeLibrary("glass");
    }

    private static Map deviceDetails = null;

    // provides a means for the user to pass platorm specific details
    // to the native glass impl. Can be null.
    // May need be called before Run.
    public static void setDeviceDetails(Map details) {
        deviceDetails = details;
    }

    protected static Map getDeviceDetails() {
        return deviceDetails;
    }
    
    static public void Run(String args[], String title, final Launchable launchable) {
        System.err.println("com.sun.glass.ui.Application.Run(String args[], String title, final Launchable launchable) is deprecated and will go away soon");
        Run(args, launchable);
    }
    
    static public void Run(String args[], final Launchable launchable) {
        Disposer.init();
        Application.application = PlatformFactory.getPlatformFactory().createApplication();
        Application.application.platform = Platform.DeterminePlatform();
        Application.application.args = args;
        
        // each concrete Application should set the app name using its own platform mechanism:
        // on Mac OS X - use NSBundle info, which can be overriden by -Xdock:name
        // on Windows - TODO
        // on Linux - TODO
        Application.application.name = "java"; // default
        
        Application.application.launchable = launchable;
        Application.application.run();
    }

    private void run() {
        try {
            runLoop(this.args, this.launchable);
        } catch (Throwable t) {
            t.printStackTrace();
        }
    }

    // runLoop never exits until app terminates
    protected abstract void runLoop(String args[], Launchable launchable);

    // should return after loop termination completion
    protected void finishTerminating() {
        // To override in subclasses
    }
    
    private void notifyWillFinishLaunching() {
        EventHandler handler = getEventHandler();
        if (handler != null) {
            handler.handleWillFinishLaunchingAction(this, System.nanoTime());
        }
    }
    
    private void notifyDidFinishLaunching() {
        EventHandler handler = getEventHandler();
        if (handler != null) {
            handler.handleDidFinishLaunchingAction(this, System.nanoTime());
        }
    }
    
    private void notifyWillBecomeActive() {
        EventHandler handler = getEventHandler();
        if (handler != null) {
            handler.handleWillBecomeActiveAction(this, System.nanoTime());
        }
    }
    
    private void notifyDidBecomeActive() {
        this.initialActiveEventReceived = true;
        EventHandler handler = getEventHandler();
        if (handler != null) {
            handler.handleDidBecomeActiveAction(this, System.nanoTime());
        }
    }
    
    private void notifyWillResignActive() {
        EventHandler handler = getEventHandler();
        if (handler != null) {
            handler.handleWillResignActiveAction(this, System.nanoTime());
        }
    }
    
    private void notifyDidResignActive() {
        EventHandler handler = getEventHandler();
        if (handler != null) {
            handler.handleDidResignActiveAction(this, System.nanoTime());
        }
    }
    
    private void notifyWillHide() {
        EventHandler handler = getEventHandler();
        if (handler != null) {
            handler.handleWillHideAction(this, System.nanoTime());
        }
    }
    
    private void notifyDidHide() {
        EventHandler handler = getEventHandler();
        if (handler != null) {
            handler.handleDidHideAction(this, System.nanoTime());
        }
    }
    
    private void notifyWillUnhide() {
        EventHandler handler = getEventHandler();
        if (handler != null) {
            handler.handleWillUnhideAction(this, System.nanoTime());
        }
    }
    
    private void notifyDidUnhide() {
        EventHandler handler = getEventHandler();
        if (handler != null) {
            handler.handleDidUnhideAction(this, System.nanoTime());
        }
    }
    
    // notificiation when user drag and drops files onto app icon
    private void notifyOpenFiles(String files[]) {
        if ((this.initialActiveEventReceived == false) && (this.initialOpenedFiles == null)) {
            // rememeber the initial opened files
            this.initialOpenedFiles = files;
        }
        EventHandler handler = getEventHandler();
        if ((handler != null) && (files != null)) {
            handler.handleOpenFilesAction(this, System.nanoTime(), files);
        }
    }
    
    /**
     * Install app's default native menus:
     * on Mac OS X - Apple menu (showing the app name) with a single Quit menu item
     * on Windows - NOP
     * on Linux - NOP
     */
    public void installDefaultMenus(MenuBar menubar) {
        // To override in subclasses
    }

    public EventHandler getEventHandler() {
        return eventHandler;
    }

    public void setEventHandler(EventHandler eventHandler) {
        boolean resendOpenFiles = ((this.eventHandler != null) && (this.initialOpenedFiles != null));
        this.eventHandler = eventHandler;
        if (resendOpenFiles == true) {
            // notify the new event handler with initial opened files
            notifyOpenFiles(this.initialOpenedFiles);
    }
    }

    private boolean terminateWhenLastWindowClosed = true;
    public final boolean shouldTerminateWhenLastWindowClosed() {
        return terminateWhenLastWindowClosed;
    }
    public final void setTerminateWhenLastWindowClosed(boolean b) {
        terminateWhenLastWindowClosed = b;
    }

    public boolean shouldUpdateWindow() {
        return false; // overridden in platform application class
    }

    public void terminate() {
        try {
            synchronized (this) {
                final Vector<Window> windows = new Vector<Window>(Window.getWindows());
                for (Window window : windows) {
                    //System.err.println("window setVisible false : "+window.getTitle());

                    // first make windows invisible
                    window.setVisible(false);
                }
                for (Window window : windows) {
                    //System.err.println("window close : "+window.getTitle());

                    // now we can close windows
                    window.close();
                }
            }
        } catch (Throwable t) {
            t.printStackTrace();
        } finally {
            finishTerminating();
        }
    }

    static public Application GetApplication() {
        return Application.application;
    }

    protected final void setEventThread(Thread thread) {
        this.eventThread = thread;
    }

    protected final Thread getEventThread() {
        return this.eventThread;
    }

    public static boolean checkEventThread(boolean throwException, String msg) {
        if (Thread.currentThread().equals(GetApplication().getEventThread())) {
            return true;
        }
        if (throwException) {
            throw new RuntimeException(msg != null ? msg :
                    "This operation is permitted on the event thread only");
        }
        return false;
    }

    public static boolean checkEventThread(boolean throwException) {
        return Application.checkEventThread(throwException, null);
    }

    public static void checkEventThread() {
        Application.checkEventThread(true);
    }

    abstract protected void _invokeAndWait(java.lang.Runnable runnable);
    /**
     * Block the current thread and wait until the given  runnable finishes
     * running on the native event loop thread.
     */
    public static void invokeAndWait(java.lang.Runnable runnable) {
        if (runnable == null) {
            return;
        }
        if (checkEventThread(false,"")) {
            runnable.run();
        } else {
            GetApplication()._invokeAndWait(runnable);
        }
    }

    abstract protected void _invokeLater(java.lang.Runnable runnable);
    /**
     * Schedule the given runnable to run on the native event loop thread
     * some time in the future, and return immediately.
     */
    public static void invokeLater(java.lang.Runnable runnable) {
        if (runnable == null) {
            return;
        }
        GetApplication()._invokeLater(runnable);
    }

    abstract protected void _postOnEventQueue(java.lang.Runnable runnable);
    /**
     * Post the runnable to the tail of the native event queue and return.
     *
     * Note that native input events (mouse, keyboard, etc.) may have lower
     * priority than posted user events, and therfore they may be dispatched
     * only after all user events have been processed.
     */
    public static void postOnEventQueue(java.lang.Runnable runnable) {
        if (runnable == null) {
            return;
        }
        GetApplication()._postOnEventQueue(runnable);
    }

    protected abstract Object _enterNestedEventLoop();
    protected abstract void _leaveNestedEventLoop(Object retValue);

    private static int nestedEventLoopCounter = 0;

    /**
     * Starts a nested event loop.
     *
     * Calling this smethod temporarily blocks processing of the current event,
     * and starts a nested event loop to handle other native events.  To
     * proceed with the blocked execution path, the application should call the
     * {@link #leaveNestedEventLoop(Object)} method.
     *
     * Note that this method may only be invoked on the main (event handling)
     * thread.
     *
     * An application may enter several nested loops recursively. There's no
     * limit of recursion other than that imposed by the native stack size.
     *
     * @return an object passed to the leaveNestedEventLoop() method
     * @throws RuntimeException if the current thread is not the main thread
     */
    public static Object enterNestedEventLoop() {
        checkEventThread();

        nestedEventLoopCounter++;
        try {
            return GetApplication()._enterNestedEventLoop();
        } finally {
            nestedEventLoopCounter--;
        }
    }

    /**
     * Terminates the current nested event loop.
     *
     * After calling this method and returning from the current event handler,
     * the execusion returns to the point where the {@link #enterNestedEventLoop}
     * was called previously. You may specify a return value for the
     * enterNestedEventLoop() method by passing the argument {@code retValue} to
     * the leaveNestedEventLoop().
     *
     * Note that this method may only be invoked on the main (event handling)
     * thread.
     *
     * @throws RuntimeException if the current thread is not the main thread
     * @throws IllegalStateException if the application hasn't started a nested
     *                               event loop
     */
    public static void leaveNestedEventLoop(Object retValue) {
        checkEventThread();

        if (nestedEventLoopCounter == 0) {
            throw new IllegalStateException("Not in a nested event loop");
        }

        GetApplication()._leaveNestedEventLoop(retValue);
    }

    public void menuAboutAction() {
        System.err.println("about");
    }


    // FACTORY METHODS

    /**
     * Create a window.
     *
     * The styleMask argument is a bitmask of window styles as defined in the
     * Window class.  Note, however, that visual kinds (UNTITLED, TITLED,
     * or TRANSPARENT) can't be combined together.  Also, functional types
     * (NORMAL, POPUP, or UTILITY) can't be combined together.  A window is
     * allowed to be of exactly one visual kind, and exactly one functional
     * type.
     */
    public abstract Window createWindow(Window owner, Screen screen, int styleMask);

    /**
     * Create a window.
     *
     * The styleMask argument is a bitmask of window styles as defined in the
     * Window class.  Note, however, that visual kinds (UNTITLED, TITLED,
     * or TRANSPARENT) can't be combined together.  Also, functional types
     * (NORMAL, POPUP, or UTILITY) can't be combined together.  A window is
     * allowed to be of exactly one visual kind, and exactly one functional
     * type.
     */
    public Window createWindow(Screen screen, int styleMask) {
        return createWindow(null, screen, styleMask);
    }

    public abstract Window createWindow(long parent);

    public abstract View createView(Pen pen);

    protected Clipboard createClipboard(String name) {
        return Clipboard.get(name);
    }

    public abstract Cursor createCursor(int type);
    public abstract Cursor createCursor(int x, int y, Pixels pixels);

    protected abstract void staticCursor_setVisible(boolean visible);
    protected abstract Size staticCursor_getBestSize(int width, int height);

    public Cursor createCursor(Pixels pixels) {
        return createCursor(pixels.getWidth() / 2, pixels.getHeight() / 2,
                pixels);
    }


    public Menu createMenu(String title) {
        return new Menu(title);
    }

    public Menu createMenu(String title, boolean enabled) {
        return new Menu(title, enabled);
    }

    public MenuBar createMenuBar() {
        return new MenuBar();
    }

    public MenuItem createMenuItem(String title) {
        return createMenuItem(title, null);
    }

    public MenuItem createMenuItem(String title, MenuItem.Callback callback) {
        return createMenuItem(title, callback, KeyEvent.VK_UNDEFINED, KeyEvent.MODIFIER_NONE);
    }

    public MenuItem createMenuItem(String title, MenuItem.Callback callback,
            int shortcutKey, int shortcutModifiers) {
        return createMenuItem(title, callback, shortcutKey, shortcutModifiers, null);
    }

    public MenuItem createMenuItem(String title, MenuItem.Callback callback,
            int shortcutKey, int shortcutModifiers, Pixels pixels) {
        return new MenuItem(title, callback, shortcutKey, shortcutModifiers, pixels);
    }

    public abstract Pixels createPixels(int width, int height, ByteBuffer data);
    public abstract Pixels createPixels(int width, int height, IntBuffer data);
    protected abstract int staticPixels_getNativeFormat();


    public abstract Robot createRobot();

    protected abstract Screen staticScreen_getDeepestScreen();
    protected abstract Screen staticScreen_getMainScreen();
    protected abstract Screen staticScreen_getScreenForLocation(int x, int y);
    protected abstract Screen staticScreen_getScreenForPtr(long screenPtr);
    protected abstract List<Screen> staticScreen_getScreens();

    public abstract Timer createTimer(Runnable runnable);
    protected abstract int staticTimer_getMinPeriod();
    protected abstract int staticTimer_getMaxPeriod();

    public final EventLoop createEventLoop() {
        return new EventLoop();
    }

    protected abstract String[] staticCommonDialogs_showFileChooser(Window owner, String folder, String title, int type,
                                                     boolean multipleMode, ExtensionFilter[] extensionFilters);

    protected abstract String staticCommonDialogs_showFolderChooser(Window owner, String folder, String title);

    protected abstract long staticView_getMultiClickTime();
    protected abstract int staticView_getMultiClickMaxX();
    protected abstract int staticView_getMultiClickMaxY();

    public abstract boolean supportsTransparentWindows();
}
