/*
 * Copyright (c) 2009 The Jackson Laboratory
 * 
 * This software was developed by Gary Churchill's Lab at The Jackson
 * Laboratory (see http://research.jax.org/faculty/churchill).
 *
 * This is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This software 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this software.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.jax.qtl;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.swing.JOptionPane;
import javax.swing.SwingUtilities;
import javax.swing.ToolTipManager;
import javax.swing.UIManager;

import org.jax.qtl.configuration.QtlApplicationConfigurationManager;
import org.jax.qtl.desktopOrganization.QtlMenubar;
import org.jax.qtl.project.QtlProjectManager;
import org.jax.qtl.project.gui.QtlProjectTree;
import org.jax.r.gui.ApplicationFrame;
import org.jax.r.jriutilities.RInterface;
import org.jax.r.jriutilities.RInterfaceFactory;
import org.jax.r.jriutilities.SilentRCommand;
import org.jax.r.rintegration.VersionStringComparator;
import org.jax.util.TextWrapper;
import org.jax.util.TypeSafeSystemProperties;
import org.jax.util.TypeSafeSystemProperties.OsFamily;
import org.jax.util.gui.desktoporganization.Desktop;
import org.jax.util.project.ProjectManager;
import org.rosuda.JRI.REXP;

/**
 * The main application class for J/qtl
 * @see QtlLauncher
 * @author Lei Wu, Keith Sheppard
 */
public class QTL
{
    /**
     * our logger
     */
    private static final Logger LOG = Logger.getLogger(
            QTL.class.getName());
    private static final String MINIMUM_RQTL_VERSION = "1.08";
    private static QTL instance;
    
    private final Desktop desktop;
    private final QtlMenubar menubar;

    private final ApplicationFrame applicationFrame;
    
    /**
     * @see #getProjectTree()
     */
    private final QtlProjectTree projectTree;
    
    /**
     * Getter for the singleton QTL instance
     * @return
     *          the singleton instance
     */
    public static QTL getInstance()
    {
        // we have 2 if's so that we don't need to waste time synchronizing
        // unless it's needed
        if(QTL.instance == null)
        {
            synchronized(QTL.class)
            {
                if(QTL.instance == null)
                {
                    QTL.instance = new QTL();
                }
            }
        }
        return QTL.instance;
    }
    
    /**
     *  constructor for initial screen
     */
    private QTL()
    {
        this.desktop = new Desktop();
        this.menubar = new QtlMenubar(this.desktop.getWindowMenu());
        
        this.projectTree = new QtlProjectTree();
        this.projectTree.setProjectManager(QtlProjectManager.getInstance());
        
        this.applicationFrame = new ApplicationFrame(
                "J/qtl - GUI for R/qtl ",
                RInterfaceFactory.getRInterfaceInstance(),
                this.menubar,
                this.desktop,
                this.projectTree,
                QtlProjectManager.getInstance());
        
        this.showGui();
        this.initialCallsToR();
        
        final QtlProjectManager projectManager = QtlProjectManager.getInstance();
        projectManager.addPropertyChangeListener(
                ProjectManager.ACTIVE_PROJECT_PROPERTY_NAME,
                new PropertyChangeListener()
                {
                    /**
                     * {@inheritDoc}
                     */
                    public void propertyChange(PropertyChangeEvent evt)
                    {
                        QTL.this.getDesktop().closeAllWindows();
                    }
                });
        
        projectManager.addPropertyChangeListener(
                ProjectManager.ACTIVE_PROJECT_FILE_PROPERTY_NAME,
                new PropertyChangeListener()
                {
                    /**
                     * {@inheritDoc}
                     */
                    public void propertyChange(PropertyChangeEvent evt)
                    {
                        QtlApplicationConfigurationManager.getInstance().notifyActiveProjectFileChanged(
                                projectManager.getActiveProjectFile());
                    }
                });
        
        // Show tool tips immediately
        ToolTipManager.sharedInstance().setInitialDelay(0);
        // Keep the tool tip showing through the whole program
        ToolTipManager.sharedInstance().setDismissDelay(Integer.MAX_VALUE);
    }
    
    /**
     * Getter for the main application frame
     * @return
     *          the application frame
     */
    public ApplicationFrame getApplicationFrame()
    {
        return this.applicationFrame;
    }

    /**
     * Getter for the menu bar
     * @return
     *          the menu bar
     */
    public QtlMenubar getMenubar()
    {
        return this.menubar;
    }

    /**
     * Getter for the desktop
     * @return
     *          the desktop
     */
    public Desktop getDesktop()
    {
        return this.desktop;
    }

    /**
     * Getter for the project tree
     * @return
     *          the project tree
     */
    public QtlProjectTree getProjectTree()
    {
        return this.projectTree;
    }

    /**
     * Execute some initialization calls to R.
     */
    private void initialCallsToR()
    {
        QtlApplicationConfigurationManager configurationManager =
            QtlApplicationConfigurationManager.getInstance();
        configurationManager.setSaveOnExit(true);
        Long rMemLimitMB =
            configurationManager.getApplicationConfiguration().getRMemoryLimitMegabytes();
        
        // intial calls to R
        final RInterface rInterface = RInterfaceFactory.getRInterfaceInstance();
        if(rMemLimitMB != null &&
           TypeSafeSystemProperties.getOsFamily() == OsFamily.WINDOWS_OS_FAMILY)
        {
            String comment =
                "Setting R memory ceiling " +
                "(you can change this in the preferences panel)";
            rInterface.insertComment(comment);
            rInterface.evaluateCommandNoReturn(
                    "memory.limit(" + rMemLimitMB.intValue() + ")");
        }
        final String qtlVersionString = this.loadRQtlAndGetVersionString();
        
        // TODO need a way of making sure this stuff doesn't get into the script
        if(qtlVersionString == null)
        {
            SwingUtilities.invokeLater(new Runnable()
            {
                public void run()
                {
                    String message =
                        "It appears that the required library R/qtl is not " +
                        "installed. Would you like me to try installing R/qtl via " +
                        "the \"install.packages(...)\" command?";
                    String[] wrappedMessage = TextWrapper.wrapText(
                            message,
                            TextWrapper.DEFAULT_DIALOG_COLUMN_COUNT);
                    int response = JOptionPane.showConfirmDialog(
                            QTL.this.applicationFrame,
                            wrappedMessage,
                            "R/qtl Not Installed",
                            JOptionPane.YES_NO_OPTION,
                            JOptionPane.WARNING_MESSAGE);
                    if(response == JOptionPane.YES_OPTION)
                    {
                        // this is long running so we shouldn't do it in the
                        // AWT thread
                        Thread installThread = new Thread(new Runnable()
                        {
                            public void run()
                            {
                                // install then validate
                                QTL.this.installRQtl();
                                QTL.this.runFinalRQtlInstallCheck();
                            }
                        });
                        installThread.start();
                    }
                    else
                    {
                        QTL.this.getApplicationFrame().closeApplication();
                    }
                }
            });
        }
        else
        {
            if(!this.isRQtlVersionValid(qtlVersionString))
            {
                SwingUtilities.invokeLater(new Runnable()
                {
                    public void run()
                    {
                        String message =
                            "The installed version of R/qtl appears to be older " +
                            "than the minimum required version (" +
                            MINIMUM_RQTL_VERSION + "). Would you like J/qtl to " +
                            "attempt to replace R/qtl " + qtlVersionString +
                            " with an updated version?";
                        String[] wrappedMessage = TextWrapper.wrapText(
                                message,
                                TextWrapper.DEFAULT_DIALOG_COLUMN_COUNT);
                        int response = JOptionPane.showConfirmDialog(
                                QTL.this.applicationFrame,
                                wrappedMessage,
                                "Outdated R/qtl Version",
                                JOptionPane.YES_NO_OPTION,
                                JOptionPane.WARNING_MESSAGE);
                        if(response == JOptionPane.YES_OPTION)
                        {
                            // this is long running so we shouldn't do it in the
                            // AWT thread
                            Thread installThread = new Thread(new Runnable()
                            {
                                public void run()
                                {
                                    // remove, install then validate
                                    rInterface.insertComment(
                                            "removing outdated r/QTL version");
                                    rInterface.evaluateCommandNoReturn(
                                            "detach(\"qtl\")");
                                    rInterface.evaluateCommand(
                                            "remove.packages(\"qtl\")");
                                    QTL.this.installRQtl();
                                    QTL.this.runFinalRQtlInstallCheck();
                                }
                            });
                            installThread.start();
                        }
                        else
                        {
                            QTL.this.getApplicationFrame().closeApplication();
                        }
                    }
                });
            }
        }
    }
    
    /**
     * Check if the R/qtl install is valid and alert the user if it is not.
     * It is assumed at this point that we have already tried to repair the
     * installation and will not try again.
     */
    private void runFinalRQtlInstallCheck()
    {
        String qtlVersionString = this.loadRQtlAndGetVersionString();
        String errorMessage = null;
        if(qtlVersionString == null)
        {
            errorMessage = "Attempted installation of R/qtl failed.";
        }
        else
        {
            if(!this.isRQtlVersionValid(qtlVersionString))
            {
                errorMessage =
                    "Failed to install a recent enough version of R/qtl. " +
                    "The installed version is " + qtlVersionString +
                    " but J/qtl requires at least version " +
                    MINIMUM_RQTL_VERSION + ".";
            }
        }
        
        if(errorMessage != null)
        {
            // the "final" is so that we can pass this to the runnable using
            // closure
            final String finalErrorMessage = errorMessage;
            SwingUtilities.invokeLater(new Runnable()
            {
                public void run()
                {
                    String[] wrappedErrorMessage = TextWrapper.wrapText(
                            finalErrorMessage,
                            TextWrapper.DEFAULT_DIALOG_COLUMN_COUNT);
                    JOptionPane.showMessageDialog(
                            QTL.this.applicationFrame,
                            wrappedErrorMessage,
                            "R/qtl Installation Failure",
                            JOptionPane.ERROR_MESSAGE);
                    QTL.this.getApplicationFrame().closeApplication();
                }
            });
            LOG.severe(errorMessage);
        }
    }

    /**
     * Determine if the given version is acceptable.
     * @param rQtlVersion
     *          the R/qtl version to check
     * @return
     *          true iff the version is good
     */
    private boolean isRQtlVersionValid(String rQtlVersion)
    {
        VersionStringComparator versionComparator =
            VersionStringComparator.getInstance();
        
        return versionComparator.compare(rQtlVersion, MINIMUM_RQTL_VERSION) >= 0;
    }
    
    /**
     * Install R/qtl using "install.packages"
     */
    private void installRQtl()
    {
        RInterface rInterface = RInterfaceFactory.getRInterfaceInstance();
        rInterface.insertComment(
                "installing required library R/qtl");
        rInterface.evaluateCommandNoReturn(
                "install.packages(pkgs=\"qtl\", repos=\"http://cran.r-project.org\")");
    }
    
    /**
     * Load R/qtl and return the version string
     * @return
     *          the version string or null if the load fails
     */
    private String loadRQtlAndGetVersionString()
    {
        final String qtlVersionCommand = "qtlversion()";
        RInterface rInterface = RInterfaceFactory.getRInterfaceInstance();
        rInterface.insertComment("load R/qtl library");
        rInterface.evaluateCommandNoReturn("library(qtl)");
        rInterface.insertComment("current R/qtl version");
        
        // this one is just so that the user can see the output. the
        // next one is for real
        rInterface.evaluateCommandNoReturn(
                qtlVersionCommand);
        REXP qtlVersionExpression = rInterface.evaluateCommand(
                new SilentRCommand(qtlVersionCommand));
        
        // validate the expression and return it as a string or null
        if(qtlVersionExpression == null)
        {
            return null;
        }
        else
        {
            String qtlVersionString = qtlVersionExpression.asString();
            if(qtlVersionString == null || qtlVersionString.contains("Error"))
            {
                return null;
            }
            else
            {
                return qtlVersionString;
            }
        }
    }

    /**
     * Create the GUI and show it.
     */
    private void showGui()
    {
        SwingUtilities.invokeLater(new Runnable()
        {
            /**
             * {@inheritDoc}
             */
            public void run()
            {
                QTL.this.applicationFrame.setVisible(true);
            }
        });
    }

    /**
     * The entry point for the J/QTL application.
     * @param args
     *      command line arguments
     */
    public static void main (String[] args)
    {
        // set the look and feel
        try
        {
            SwingUtilities.invokeAndWait(new Runnable()
            {
                /**
                 * {@inheritDoc}
                 */
                public void run()
                {
                    try
                    {
                        UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
//                        UIManager.setLookAndFeel("net.sourceforge.napkinlaf.NapkinLookAndFeel");
                    }
                    catch(Exception ex)
                    {
                        LOG.log(Level.WARNING, "failed to set system look-and-feel", ex);
                    }
                }
            });
        }
        catch(Exception ex)
        {
            LOG.log(Level.WARNING,
                    "caught exception trying to set look-and-feel",
                    ex);
        }

        QTL.writeSystemConfigurationToLog();
        QTL.getInstance();
    }

    private static int BYTES_PER_MEGABYTE = (1<<20);
    
    /**
     * Log some system configuration information
     */
    private static void writeSystemConfigurationToLog()
    {
        if(LOG.isLoggable(Level.FINE))
        {
            long maxMemoryBytes = Runtime.getRuntime().maxMemory();
            if(maxMemoryBytes % BYTES_PER_MEGABYTE == 0)
            {
                // memory allocation is divisible by megabytes
                LOG.fine(
                        "Maximum Java Memory: " +
                        (Runtime.getRuntime().maxMemory() / (BYTES_PER_MEGABYTE)) +
                        "MB");
            }
            else
            {
                LOG.fine(
                        "Maximum Java Memory: " +
                        Runtime.getRuntime().maxMemory() +
                        " bytes");
            }
            
            LOG.fine(
                    "java.library.path=" +
                    TypeSafeSystemProperties.getJavaLibraryPath());
            LOG.fine("Environment Variables:");
            for(Map.Entry<String, String> currEnvEntry: System.getenv().entrySet())
            {
                LOG.fine(currEnvEntry.getKey() + "=" + currEnvEntry.getValue());
            }
        }
    }
}
