/*
 * DefaultSwingApplication.java  $Revision: 1.20 $  $Date: 2007/10/03 04:39:40 $
 *
 * Copyright (C) 2007 Meldcraft Corporation
 *
 * This file is part of Scaffold.
 * 
 * Scaffold is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Scaffold is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with Scaffold.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.meldcraft.application.swing;

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Graphics;
import java.awt.Window;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.ByteArrayInputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.swing.*;

import com.meldcraft.gui.swing.SwingUtil;
import com.meldcraft.application.*;
import com.meldcraft.application.swing.action.ActionGUIFactory;
import com.meldcraft.application.util.BeanUtil;
import com.meldcraft.util.ConfigurationManager;
import com.meldcraft.util.CountDownTimer;
import com.meldcraft.util.PropertiesHelper;
import com.meldcraft.util.logging.LogUtil;

/**
 * @version $Revision: 1.20 $ $Date: 2007/10/03 04:39:40 $
 * @author Rob Richards
 */
public class DefaultSwingApplication extends AbstractApplication
{
// Static variables
// Member variables
    private boolean                 mInitialized = false;
    private Object                  mInitLock = new Object();
    private JWindow                 mSplashWindow;
    private SwingApplicationContext mApplicationContext;
    private boolean                 mClosable = true;

    protected Logger                mLogger;

// Constructors and initialization
// Interface implemenations
    // Application interface
    public ApplicationContext getApplicationContext()
    {
        return mApplicationContext;
    }

    /**
     * This implementation performs the following (in order):
     * <ul>
     * <li>creates a logger, via createLogger()</li>
     * <li>calls showSplash() (with a true argument)</li>
     * <li>creates the UI, via createApplicationRoot()</li>
     * <li>creates the ApplicationContext, via createApplicationContext()</li>
     * <li>calls the configureApplicationRoot() method</li>
     * <li>calls the configureListeners() method</li>
     * <li>calls showSplash() (with a false argument)</li>
     * <li>attempts to make the UI visible, and bring it to the front of the screen</li>
     * </ul>
     */
    public void applicationInitialize(String appName)
    {
        synchronized(mInitLock)
        {
            if (! mInitialized)
            {
                mInitialized = true;

                Map config = ConfigurationManager.getConfiguration(appName);

                mLogger = createLogger(config);

                boolean splash = showSplash(true, config);

                CountDownTimer timer = null;
                if (splash)
                {
                    long delay = PropertiesHelper.getLong(config, "Application.splashDuration", 1000);
                    if (delay > 0)
                    {
                        timer = new CountDownTimer(delay);
                    }
                }

                mApplicationContext = createApplicationContext(config);
                initializeInvokers();
                configureApplicationRoot(mApplicationContext.getUIRoot());
                configureCloser();

                if (timer != null)
                {
                    timer.waitForCountDown();
                }
                showSplash(false, config);

                mApplicationContext.contextChanged("Application.initialized", null);

                SwingUtil.bringToFront(mApplicationContext.getUIRoot());
            }
        }
    }

    /**
     * Invokes the dispose() method on the UI Window.  If called in a
     * thread other than the Event Dispatch Thread, the call to dispose()
     * is performed using SwingUtilities.invokeLater().
     */
    public void applicationClose() throws ApplicationClosingNotAllowedException
    {
        super.applicationClose();

        checkApplicationClose();

        SwingUtil.invokeLater(new Runnable() {
            public void run()
            {
                Window window = SwingUtil.getWindow(mApplicationContext.getUIRoot());
                window.dispose();
            }
        });
    }

    /**
     * Brings GUI to front, then sends ApplicationContext event
     * Open.invokeReturn with arg.
     */
    public void applicationOpen(Object arg)
    {
        SwingUtil.bringToFront(mApplicationContext.getUIRoot());

        if (arg instanceof String[])
        {
            String[] args = (String[])arg;
            if (args.length == 0)
            {
                arg = null;
            }
            else if (args.length == 1)
            {
                arg = args[0];
            }
        }

        if (arg != null)
        {
            getApplicationContext().contextChanged("Open.invokeReturn", arg);
        }
    }

// Public methods
// Protected methods
    /**
     * Creates a Logger for use in the ApplicationContext.
     */
    protected Logger createLogger(Map config)
    {
        return LogUtil.getLogger(this);
    }

    /**
     * Creates the ApplicationContext for this Application.
     * @param config
     * @return
     */
    protected SwingApplicationContext createApplicationContext(Map config)
    {
        return new SwingApplicationContext(this, config);
    }

    /**
     * Invoked after the application context and ui is created.  It is
     * intended that this method populate the ui (e.g. add the content
     * Components) here.  This implementation adds the menu bar and
     * toolbar as configured in the application properties file, and
     * centers the ui on the screen.
     * 
     * @param ui The Window returned by the createUI(Map) method
     */
    protected void configureApplicationRoot(Component ui)
    {
        JFrame frame = (JFrame)ui;

        JMenuBar menuBar = createJMenuBar();
        if (menuBar != null)
        {
            frame.setJMenuBar(menuBar);
        }

        JToolBar toolBar = createJToolBar();
        if (toolBar != null)
        {
            frame.getContentPane().add(toolBar, BorderLayout.NORTH);
        }

        configureApplicationActions(ui);

        // set icon if available
        try
        {
            String name = PropertiesHelper.getString(mApplicationContext.getConfiguration(), ConfigurationManager.CONFIGURATION_BASE_NAME, getClass().getName());
            String sname = name.substring(name.lastIndexOf(".")+1, name.length());
            ImageIcon img = SwingUtil.loadResourceImage(getClass().getClassLoader(), name, sname, "32");
            if (img == null)
            {
                img = SwingUtil.loadResourceImage(getClass().getClassLoader(), name, sname, "16");
            }

            if (img != null)
            {
                frame.setIconImage(img.getImage());
            }
        }
        catch (Exception e)
        {
            mLogger.log(Level.WARNING, "Error loading frame image icon.", e);
        }

        // configure frame
        frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);

        frame.addWindowListener(new WindowAdapter() {
            public void windowClosing (WindowEvent e)
            {
                try
                {
                    applicationClose();
                }
                catch (ApplicationClosingNotAllowedException ae)
                {
                    // ignored
                }
            }

            public void windowClosed (WindowEvent e)
            {
                getApplicationContext().contextChanged(Application.APPLICATION_CLOSED_KEY, null);
            }
        });

        Component content = createApplicationRootContent();
        if (content != null)
        {
            frame.getContentPane().add(content, BorderLayout.CENTER);
        }

        Map config = getApplicationContext().getConfiguration();
        int width = 0;
        int height = 0;
        String size = PropertiesHelper.getString(config, "size", null);
        if (size != null)
        {
            String[] wh = size.trim().split(",");
            if ((wh != null) && (wh.length == 2))
            {
                width = PropertiesHelper.getInt(wh[0].trim(), 0);
                height = PropertiesHelper.getInt(wh[1].trim(), 0);
            }
            else if ((wh != null) && (wh.length == 1))
            {
                width = PropertiesHelper.getInt(wh[0].trim(), 0);
            }
        }
        if ((width > 0) || (height > 0))
        {
            frame.pack();
            if (width == 0)
            {
                width = frame.getWidth();
            }
            if (height == 0)
            {
                height = frame.getHeight();
            }
            SwingUtil.centerOnScreen(frame, width, height, true);
        }
        else
        {
            frame.pack();
            SwingUtil.centerOnScreen(frame);
        }
    }

    protected void configureApplicationActions(Component ui)
    {
        String[] actions = PropertiesHelper.getStringArray(mApplicationContext.getConfiguration(), "Application.mainactions");
        if (actions != null)
        {
            JComponent rootComponent = null;
            if (ui instanceof JComponent)
            {
                rootComponent = (JComponent)ui;
            }
            else
            {
                Object obj = BeanUtil.getPropertyValue(ui, "rootPane");
                if (obj instanceof JComponent)
                {
                    rootComponent = (JComponent)obj;
                }
            }

            if (rootComponent != null)
            {
                for (int ia=0 ; ia < actions.length ; ia++)
                {
                    Action action = getSwingApplicationContext().getActionManager().getAction(actions[ia]);
                    if (action != null)
                    {
                        KeyStroke[] accels = (KeyStroke[])action.getValue(Action.ACCELERATOR_KEY + "s");
                        if (accels != null)
                        {
                            for (int i=0 ; i < accels.length ; i++)
                            {
                                KeyStroke accelerator = accels[i];
                                if (accelerator != null)
                                {
                                    rootComponent.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(accelerator, "doClick");
                                }
                            }
                            rootComponent.getActionMap().put("doClick", action);
                        }
                        else
                        {
                            KeyStroke accelerator = (KeyStroke)action.getValue(Action.ACCELERATOR_KEY);
                            if (accelerator != null)
                            {
                                rootComponent.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(accelerator, "doClick");
                                rootComponent.getActionMap().put("doClick", action);
                            }
                        }
                    }
                }
            }
        }
    }

    /**
     * Invoked by configureApplictionRoot() to create the frame content.
     * 
     * @param ui The Window returned by the createUI(Map) method
     */
    protected Component createApplicationRootContent()
    {
        Component ret = null;
        String name = PropertiesHelper.getString(mApplicationContext.getConfiguration(), "Application.rootContent", null);
        if (name != null)
        {
            name = name.trim();
            ret = (Component)mApplicationContext.getElementManager().getApplicationElement(name);
            if (ret == null)
            {
                Map map = PropertiesHelper.getSubset(mApplicationContext.getConfiguration(), name + ".");
                if (map.size() > 0)
                {
                    ret = ActionGUIFactory.getInstance(mApplicationContext).createButton(name);
                }
            }
        }
        return ret;
    }

    /**
     * Shows or hides a splash screen.  An image resource with the same
     * name of this application plus a suffix of "_Splash", and an extension
     * of .png, .gif, .jpg, .jpeg will be used.  If no image is found, no
     * splash screen will be displayed.
     * 
     * @param show
     * @param config
     */
    protected boolean showSplash(boolean show, Map config)
    {
        boolean showing = false;

        if (show)   
        {
            if (mSplashWindow == null)
            {
            	String name = PropertiesHelper.getString(config, ConfigurationManager.CONFIGURATION_BASE_NAME, getClass().getName());
            	String sname = name.substring(name.lastIndexOf(".")+1, name.length());
                ImageIcon img = SwingUtil.loadResourceImage(getClass().getClassLoader(), name, sname, "_Splash");
                if (img != null)
                {
                    SplashLabel label = new SplashLabel(img, config, mLogger);
                    mSplashWindow = new JWindow();
                    mSplashWindow.getContentPane().add(label);
                    mSplashWindow.pack();
                    SwingUtil.centerOnScreen(mSplashWindow);
                }
            }
        }

        if (mSplashWindow != null)
        {
            mSplashWindow.setVisible(show);
            showing = true;
        }

        return showing;
    }

    protected JMenuBar createJMenuBar()
    {
        JMenuBar ret = ActionGUIFactory.getInstance(mApplicationContext).
                                            createJMenuBar(SwingApplicationConstants.APP_MENU_KEY);
        if (ret.getMenuCount() == 0)
        {
            ret = null;
        }

        return ret;
    }

    protected JToolBar createJToolBar()
    {
        JToolBar ret = ActionGUIFactory.getInstance(mApplicationContext).
                                            createJToolBar(SwingApplicationConstants.APP_TOOLBAR_KEY);
        if (ret != null)
        {
            boolean floatable = PropertiesHelper.getBoolean(mApplicationContext.getConfiguration(), SwingApplicationConstants.APP_TOOLBAR_KEY + ".floatable", false);
            ret.setFloatable(floatable);
        }
        return ret;
    }

    protected SwingApplicationContext getSwingApplicationContext()
    {
        return mApplicationContext;
    }

    protected void initializeInvokers()
    {
        Map map = PropertiesHelper.getMap(mApplicationContext.getConfiguration(), "Application.contextListeners");
        if ((map != null) && (map.size() > 0))
        {
            String[] keys = (String[])map.get(PropertiesHelper.ORDERED_KEYS);
            for (int iter=0 ; iter < 2 ; iter++)
            {
                for (int i=0 ; i < keys.length ; i++)
                {
                    String key = keys[i];
                    if (iter == 0)
                    {
                        if (!key.equals("ALL"))
                        {
                            continue;
                        }
                    }
                    else if (key.equals("ALL"))
                    {
                        continue;
                    }

                    String[] vals = PropertiesHelper.getStringArray((String)map.get(key));
                    addInvoker(key, vals);
                }
            }
        }

        map = PropertiesHelper.getSubset(mApplicationContext.getConfiguration(), "Application.contextListener.");
        if (map != null)
        {
            Object[] keys = map.keySet().toArray();
            for (int i=0 ; i < keys.length ; i++)
            {
                Object key = keys[i];
                if (key instanceof String)
                {
                    String[] vals = PropertiesHelper.getStringArray(map, (String)key);
                    addInvoker((String)key, vals);
                }
            }
        }
    }

    protected void configureCloser()
    {
        mApplicationContext.addApplicationContextListener(new ClosableListener(), "Application.Closable");
    }

    protected void checkApplicationClose() throws ApplicationClosingNotAllowedException
    {
        if (!mClosable)
        {
            throw new ApplicationClosingNotAllowedException();
        }
    }

// Private methods
    private void addInvoker(String key, String[] vals)
    {
        for (int j=0 ; j < vals.length ; j++)
        {
            String val = vals[j];
            HashMap defaults = new HashMap();
            defaults.put("className", SwingApplicationContextChangedInvoker.class.getName());
            try
            {
                ApplicationContextListener listener = (ApplicationContextListener)ManagedElementFactory.getInstance(mApplicationContext).createManagedElement(val, defaults, null);
                if (key.equals("ALL"))
                {
                    mApplicationContext.addApplicationContextListener(listener);
                }
                else
                {
                    mApplicationContext.addApplicationContextListener(listener, key);
                }
            }
            catch (Throwable e)
            {
                mLogger.log(Level.WARNING, "Error initializing patch bay for " + key, e);
            }
        }
    }

// Inner classes
    public static class ClosableListener implements ApplicationContextListener
    {
        public void applicationContextChanged(ApplicationContextEvent e)
        {
            DefaultSwingApplication app = (DefaultSwingApplication)e.getApplicationContext().getApplication();
            boolean val = PropertiesHelper.getBoolean(e.getValue(), true);
            app.mClosable = val;
        }
    }

    public static class SplashLabel extends JLabel
    {
        private Map     mConfig;
        private Logger  mLogger;

        public SplashLabel(Icon icon, Map config, Logger logger)
        {
            super(icon);
            mConfig = config;
            mLogger = logger;
        }

        public void paintComponent(Graphics g)
        {
            super.paintComponent(g);

            String[] keys = {SwingApplicationConstants.APP_VERSION_KEY,
                             SwingApplicationConstants.APP_COPYRIGHT_KEY};
            for (int i=0 ; i < keys.length ; i++)
            {
                String key = keys[i];
                String str = PropertiesHelper.getString(mConfig, key, null);
                if (str != null)
                {
                    // get location from application properties
                    String locstr = PropertiesHelper.getString(mConfig, key + ".splash.location", null);
                    if (locstr == null)
                    {
                        // get location from image comments
                        ImageIcon icon = (ImageIcon)getIcon();
                        Object comment = icon.getImage().getProperty("comment", icon.getImageObserver());
                        if (comment instanceof String)
                        {
                            try
                            {
                                Map  props = PropertiesHelper.load(new ByteArrayInputStream(comment.toString().getBytes()));
                                locstr = PropertiesHelper.getString(props, key + ".location", null);
                            }
                            catch (Exception e)
                            {
                                mLogger.log(Level.WARNING, "Could not extract comment from image.", e);
                            }
                        }
                    }

                    if (locstr != null)
                    {
                        try
                        {
                            Location loc = new Location(locstr);
                            String pre = "";
                            if (key.equals(SwingApplicationConstants.APP_VERSION_KEY))
                            {
                                pre = "Version ";
                            }
                            drawString(g, pre + str, loc);
                        }
                        catch (Exception e)
                        {
                            mLogger.log(Level.WARNING, "Error with location.", e);
                        }
                    }
                }
            }
        }

        public void drawString(Graphics g, String str, Location loc)
        {
            int width = g.getFontMetrics().stringWidth(str);
            int height = g.getFontMetrics().getAscent();
            int x = loc.getMinX(width);
            int y = loc.getMaxY(height);
            g.drawString(str, x,y);
        }
    }

    public static class Location
    {
        public static final String  CENTER = "C";
        public static final String  NORTH = "N";
        public static final String  EAST = "E";
        public static final String  SOUTH = "S";
        public static final String  WEST = "W";
        public static final String  NORTH_WEST = "NW";
        public static final String  NORTH_EAST = "NE";
        public static final String  SOUTH_WEST = "SW";
        public static final String  SOUTH_EAST = "SE";

        protected static final String[] REFS = {
                                CENTER, NORTH, EAST, SOUTH, WEST,
                                NORTH_WEST, NORTH_EAST, SOUTH_WEST, SOUTH_EAST};

        private String  mRef;
        private int     x;
        private int     y;

        public Location(String str)
        {
            int idx = str.indexOf(":");
            String ref = str.substring(0, idx);
            for (int i=0 ; i < REFS.length ; i++)
            {
                if (REFS[i].equals(ref))
                {
                    mRef = REFS[i];
                    break;
                }
            }
            if (mRef == null)
            {
                throw new IllegalArgumentException("Invalid location reference: "+str);
            }

            int cidx = str.indexOf(",");
            x = Integer.parseInt(str.substring(idx+1,cidx));
            y = Integer.parseInt(str.substring(cidx+1, str.length()));
        }

        public int getMinX(int width)
        {
            int ret = x;
            if ((mRef.equals(CENTER)) ||
                (mRef.equals(NORTH)) ||
                (mRef.equals(SOUTH)))
            {
                ret = x - width / 2;
            }
            else if ((mRef.equals(EAST)) ||
                     (mRef.equals(NORTH_EAST)) ||
                     (mRef.equals(SOUTH_EAST)))
            {
                ret = x - width;
            }

            return ret;
        }

        public int getMaxY(int height)
        {
            int ret = y;
            if ((mRef.equals(NORTH)) || (mRef.equals(NORTH_EAST)) || (mRef.equals(NORTH_WEST)))
            {
                ret = y + height;
            }
            else if ((mRef.equals(EAST)) || (mRef.equals(WEST)) || (mRef.equals(CENTER)))
            {
                ret = y + height / 2;
            }

            return ret;
        }

        public boolean equals(Object obj)
        {
            boolean ret = false;
            if (obj instanceof Location)
            {
                return hashCode() == obj.hashCode();
            }
            return ret;
        }

        public int hashCode()
        {
            return toString().hashCode();
        }

        public String toString()
        {
            return mRef + ":" + x + "," + y;
        }
    }
}
