/*
 * Copyright 2010-2013 Institut Pasteur.
 * 
 * This file is part of Icy.
 * 
 * Icy 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.
 * 
 * Icy 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 Icy. If not, see <http://www.gnu.org/licenses/>.
 */
package icy.system;

import icy.file.FileUtil;
import icy.type.collection.CollectionUtil;
import icy.util.ReflectionUtil;

import java.awt.BufferCapabilities;
import java.awt.Desktop;
import java.awt.DisplayMode;
import java.awt.Event;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.HeadlessException;
import java.awt.ImageCapabilities;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.VolatileImage;
import java.io.File;
import java.lang.management.ManagementFactory;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Properties;

import sun.java2d.SunGraphicsEnvironment;

import com.sun.management.OperatingSystemMXBean;

/**
 * @author stephane
 */
public class SystemUtil
{
    public static final String SYSTEM_WINDOWS = "win";
    public static final String SYSTEM_MAC_OS = "mac";
    public static final String SYSTEM_UNIX = "unix";

    /**
     * internals
     */
    private static Properties props = System.getProperties();

    private static long lastNano = 0;
    private static long lastCpu = 0;
    private static int lastCpuLoad = 0;

    /**
     * Launch specified jar file.
     * 
     * @param jarPath
     *        jar file path.
     * @param vmArgs
     *        arguments for the java virtual machine.
     * @param appArgs
     *        arguments for jar application.
     * @param workDir
     *        working directory.
     */
    public static Process execJAR(String jarPath, String vmArgs, String appArgs, String workDir)
    {
        return exec("java " + vmArgs + " -jar " + jarPath + " " + appArgs, workDir);
    }

    /**
     * Launch specified jar file.
     * 
     * @param jarPath
     *        jar file path.
     * @param vmArgs
     *        arguments for the java virtual machine.
     * @param appArgs
     *        arguments for jar application.
     */
    public static Process execJAR(String jarPath, String vmArgs, String appArgs)
    {
        return exec("java " + vmArgs + " -jar " + jarPath + " " + appArgs);
    }

    /**
     * Launch specified jar file.
     * 
     * @param jarPath
     *        jar file path.
     * @param appArgs
     *        arguments for jar application.
     */
    public static Process execJAR(String jarPath, String appArgs)
    {
        return execJAR(jarPath, "", appArgs);
    }

    /**
     * Launch specified jar file.
     * 
     * @param jarPath
     *        jar file path.
     */
    public static Process execJAR(String jarPath)
    {
        return execJAR(jarPath, "", "");
    }

    /**
     * Execute a system command and return the attached process.
     * 
     * @param cmd
     *        system command to execute.
     */
    public static Process exec(String cmd)
    {
        return exec(cmd, ".");
    }

    /**
     * Execute a system command and return the attached process.
     * 
     * @param cmd
     *        system command to execute.
     * @param dir
     *        the working directory of the subprocess, or null if the subprocess should inherit the
     *        working directory of the current process.
     */
    public static Process exec(String cmd, String dir)
    {
        try
        {
            return Runtime.getRuntime().exec(cmd, null, new File(dir));
        }
        catch (Exception e)
        {
            System.err.println("SystemUtil.exec(" + cmd + ") error :");
            IcyExceptionHandler.showErrorMessage(e, false);
            return null;
        }
    }

    public static BufferedImage createCompatibleImage(int width, int height)
    {
        return getDefaultGraphicsConfiguration().createCompatibleImage(width, height);
    }

    public static BufferedImage createCompatibleImage(int width, int height, int transparency)
    {
        return getDefaultGraphicsConfiguration().createCompatibleImage(width, height, transparency);
    }

    public static VolatileImage createCompatibleVolatileImage(int width, int height)
    {
        return getDefaultGraphicsConfiguration().createCompatibleVolatileImage(width, height);
    }

    public static VolatileImage createCompatibleVolatileImage(int width, int height, int transparency)
    {
        return getDefaultGraphicsConfiguration().createCompatibleVolatileImage(width, height, transparency);
    }

    public static Desktop getDesktop()
    {
        if (Desktop.isDesktopSupported())
            return Desktop.getDesktop();

        return null;
    }

    /**
     * @see System#getProperty(String)
     */
    public static String getProperty(String name)
    {
        return props.getProperty(name);
    }

    /**
     * @see System#getProperty(String, String)
     */
    public static String getProperty(String name, String defaultValue)
    {
        return props.getProperty(name, defaultValue);
    }

    /**
     * @see System#setProperty(String, String)
     */
    public static String setProperty(String name, String value)
    {
        return (String) props.setProperty(name, value);
    }

    /**
     * @deprecated Use {@link #getMenuCtrlMask()} instead.
     */
    @Deprecated
    public static int getCtrlMask()
    {
        return getMenuCtrlMask();
    }

    /**
     * Return the CTRL key mask used for Menu shortcut.
     */
    public static int getMenuCtrlMask()
    {
        try
        {
            return Toolkit.getDefaultToolkit().getMenuShortcutKeyMask();
        }
        catch (HeadlessException e)
        {
            // headless mode, use default Ctrl Mask
            return Event.CTRL_MASK;
        }
    }

    /**
     * @deprecated Use {@link #getMenuCtrlMask()} instead
     */
    @Deprecated
    public static int getSystemCtrlMask()
    {
        return getMenuCtrlMask();
    }

    public static GraphicsEnvironment getLocalGraphicsEnvironment()
    {
        return GraphicsEnvironment.getLocalGraphicsEnvironment();
    }

    /**
     * Return the default screen device.
     */
    public static GraphicsDevice getDefaultScreenDevice()
    {
        try
        {
            return getLocalGraphicsEnvironment().getDefaultScreenDevice();
        }
        catch (HeadlessException e)
        {
            return null;
        }
    }

    /**
     * Return the default graphics configuration.
     */
    public static GraphicsConfiguration getDefaultGraphicsConfiguration()
    {
        try
        {
            return getDefaultScreenDevice().getDefaultConfiguration();
        }
        catch (Exception e)
        {
            return null;
        }
    }

    /**
     * @deprecated Use {@link #getDefaultGraphicsConfiguration()} instead.
     */
    @Deprecated
    public static GraphicsConfiguration getSystemGraphicsConfiguration()
    {
        return getDefaultGraphicsConfiguration();
    }

    /**
     * Return the number of screen device.
     */
    public static int getScreenDeviceCount()
    {
        try
        {
            return getLocalGraphicsEnvironment().getScreenDevices().length;
        }
        catch (HeadlessException e)
        {
            return 0;
        }
    }

    /**
     * Return the screen device corresponding to specified index.
     */
    public static GraphicsDevice getScreenDevice(int index)
    {
        try
        {
            return getLocalGraphicsEnvironment().getScreenDevices()[index];
        }
        catch (HeadlessException e)
        {
            return null;
        }
    }

    /**
     * Returns true if current system is "head less" (no screen output device).
     */
    public static boolean isHeadLess()
    {
        return GraphicsEnvironment.isHeadless();
    }

    public static ClassLoader getContextClassLoader()
    {
        return Thread.currentThread().getContextClassLoader();
    }

    public static ClassLoader getSystemClassLoader()
    {
        return ClassLoader.getSystemClassLoader();
    }

    public static BufferCapabilities getSystemBufferCapabilities()
    {
        return getDefaultGraphicsConfiguration().getBufferCapabilities();
    }

    public static ImageCapabilities getSystemImageCapabilities()
    {
        return getDefaultGraphicsConfiguration().getImageCapabilities();
    }

    public static ColorModel getSystemColorModel()
    {
        return getDefaultGraphicsConfiguration().getColorModel();
    }

    public static ColorModel getSystemColorModel(int transparency)
    {
        return getDefaultGraphicsConfiguration().getColorModel(transparency);
    }

    /**
     * Return the entire desktop bounds (take multi screens in account)
     */
    public static Rectangle getDesktopBounds()
    {
        Rectangle result = new Rectangle();
        final GraphicsDevice[] gs = getLocalGraphicsEnvironment().getScreenDevices();

        for (int j = 0; j < gs.length; j++)
            result = result.union(SunGraphicsEnvironment.getUsableBounds(gs[j]));

        return result;
    }

    /**
     * {@link GraphicsEnvironment#getMaximumWindowBounds()}
     */
    public static Rectangle getMaximumWindowBounds()
    {
        return getLocalGraphicsEnvironment().getMaximumWindowBounds();
    }

    public static DisplayMode getSystemDisplayMode()
    {
        return getDefaultScreenDevice().getDisplayMode();
    }

    /**
     * @deprecated Use {@link #getNumberOfCPUs()} instead
     */
    @Deprecated
    public static int getAvailableProcessors()
    {
        return Runtime.getRuntime().availableProcessors();
    }

    /**
     * Return total number of processors or cores available to the JVM (same as system)
     */
    public static int getNumberOfCPUs()
    {
        return Runtime.getRuntime().availableProcessors();
    }

    /**
     * Return total amount of free memory available to the JVM
     */
    public static long getJavaFreeMemory()
    {
        return Runtime.getRuntime().freeMemory();
    }

    /**
     * Return maximum amount of memory the JVM will attempt to use
     */
    public static long getJavaMaxMemory()
    {
        return Runtime.getRuntime().maxMemory();
    }

    /**
     * Return total memory currently in use by the JVM
     */
    public static long getJavaTotalMemory()
    {
        return Runtime.getRuntime().totalMemory();
    }

    private static OperatingSystemMXBean getOSMXBean()
    {
        final java.lang.management.OperatingSystemMXBean bean = ManagementFactory.getOperatingSystemMXBean();

        if (bean instanceof OperatingSystemMXBean)
            return (OperatingSystemMXBean) bean;

        // for (Method method : bean.getClass().getDeclaredMethods())
        // {
        // final int modifiers=method.getModifiers();
        //
        // method.setAccessible(true);
        // if (method.getName().startsWith("get")
        // && Modifier.isPublic(modifiers)) {
        // Object value;
        // try {
        // value = method.invoke(bean);
        // } catch (Exception e) {
        // value = e;
        // } // try
        // System.out.println(method.getName() + " = " + value);
        // } // if
        // } // for

        return null;
    }

    /**
     * Return total physic memory of system (in bytes)
     */
    public static long getTotalMemory()
    {
        final OperatingSystemMXBean bean = getOSMXBean();

        if (bean != null)
            return bean.getTotalPhysicalMemorySize();

        return -1L;
    }

    /**
     * @deprecated Use {@link #getTotalMemory()} instead
     */
    @Deprecated
    public static long getSystemTotalMemory()
    {
        return getTotalMemory();
    }

    /**
     * Return free physic memory of system (in bytes)
     */
    public static long getFreeMemory()
    {
        final OperatingSystemMXBean bean = getOSMXBean();

        if (bean != null)
            return bean.getFreePhysicalMemorySize();

        return -1L;
    }

    /**
     * @deprecated Use {@link #getFreeMemory()} instead
     */
    @Deprecated
    public static long getSystemFreeMemory()
    {
        return getFreeMemory();
    }

    /**
     * Return system process CPU time
     */
    public static long getProcessCpuTime()
    {
        final OperatingSystemMXBean bean = getOSMXBean();

        if (bean != null)
            return bean.getProcessCpuTime();

        return -1L;
    }

    /**
     * @deprecated Use {@link #getProcessCpuTime()} instead
     */
    @Deprecated
    public static long getSystemProcessCpuTime()
    {
        return getProcessCpuTime();
    }

    /**
     * Return average CPU load of the application processes from the last call<br>
     * (-1 if no available)
     */
    public static int getCpuLoad()
    {
        final OperatingSystemMXBean bean = getOSMXBean();

        if (bean != null)
        {
            // first call
            if (lastNano == 0)
            {
                lastNano = System.nanoTime();
                lastCpu = bean.getProcessCpuTime();
            }
            else
            {
                final long nanoAfter = System.nanoTime();
                final long cpuAfter = bean.getProcessCpuTime();

                final long dNano = nanoAfter - lastNano;
                final long dCpu = cpuAfter - lastCpu;

                // below 0.5s the reported value isn't very significant
                if (dNano > 500000000L)
                {
                    lastCpuLoad = (int) ((dCpu * 100L) / (dNano * getNumberOfCPUs()));
                    lastNano = nanoAfter;
                    lastCpu = cpuAfter;
                }
            }

            return lastCpuLoad;
        }

        return -1;
    }

    /**
     * @deprecated Use {@link #getCpuLoad()} instead
     */
    @Deprecated
    public static int getSystemCpuLoad()
    {
        return getCpuLoad();
    }

    /**
     * Returns the user name.
     */
    public static String getUserName()
    {
        return getProperty("user.name");
    }

    /**
     * Returns the JVM name.
     */
    public static String getJavaName()
    {
        return getProperty("java.runtime.name");
    }

    /**
     * Returns the JVM version.
     */
    public static String getJavaVersion()
    {
        return getProperty("java.runtime.version");
    }

    /**
     * Returns the JVM data architecture model.
     */
    public static int getJavaArchDataModel()
    {
        return Integer.parseInt(getProperty("sun.arch.data.model"));
    }

    /**
     * Returns the Operating System name.
     */
    public static String getOSName()
    {
        return getProperty("os.name");
    }

    /**
     * Returns the Operating System architecture name.
     */
    public static String getOSArch()
    {
        return getProperty("os.arch");
    }

    /**
     * Returns the Operating System version.
     */
    public static String getOSVersion()
    {
        return getProperty("os.version");
    }

    /**
     * Return an id OS string :<br>
     * <br>
     * Windows system return <code>SystemUtil.SYSTEM_WINDOWS</code><br>
     * MAC OS return <code>SystemUtil.SYSTEM_MAC_OS</code><br>
     * Unix system return <code>SystemUtil.SYSTEM_UNIX</code><br>
     * <br>
     * An empty string is returned is OS is unknown.
     */
    public static String getOSNameId()
    {
        if (isWindow())
            return SYSTEM_WINDOWS;
        if (isMac())
            return SYSTEM_MAC_OS;
        if (isUnix())
            return SYSTEM_UNIX;

        return "";
    }

    /**
     * Return an id OS architecture string<br>
     * example : "win32", "win64", "mac32", "mac64", "unix32"...<br>
     * The bits number depends only from current installed JVM (32 or 64 bit)
     * and not directly from host OS.<br>
     * An empty string is returned if OS is unknown.
     */
    public static String getOSArchIdString()
    {
        final String javaBit = Integer.toString(getJavaArchDataModel());

        if (isWindow())
            return SYSTEM_WINDOWS + javaBit;
        if (isMac())
            return SYSTEM_MAC_OS + javaBit;
        if (isUnix())
            return SYSTEM_UNIX + javaBit;

        return "";
    }

    /**
     * Returns true is the operating system support link (symbolic or not).
     */
    public static boolean isLinkSupported()
    {
        return isMac() || isUnix();
    }

    /**
     * Returns true is the JVM is 32 bits.
     */
    public static boolean is32bits()
    {
        return getJavaArchDataModel() == 32;
    }

    /**
     * Returns true is the JVM is 64 bits.
     */
    public static boolean is64bits()
    {
        return getJavaArchDataModel() == 64;
    }

    /**
     * Returns true is the Operating System is Windows based.
     */
    public static boolean isWindow()
    {
        return (getOSName().toLowerCase().indexOf("win") >= 0);
    }

    /**
     * Returns true is the Operating System is Mac OS based.
     */
    public static boolean isMac()
    {
        return (getOSName().toLowerCase().indexOf("mac") >= 0);
    }

    /**
     * Returns true is the Operating System is Unix / Linux based.
     */
    public static boolean isUnix()
    {
        final String os = getOSName().toLowerCase();
        return (os.indexOf("nix") >= 0) || (os.indexOf("nux") >= 0);
    }

    /**
     * Returns default temporary directory.<br>
     * ex:<br>
     * <code>c:/temp</code><br>
     * <code>/tmp</code><br>
     * Same as {@link FileUtil#getTempDirectory()}
     */
    public static String getTempDirectory()
    {
        return FileUtil.getTempDirectory();
    }

    /**
     * Returns temporary native library path (used to load native libraries from plugin)
     */
    public static String getTempLibraryDirectory()
    {
        return FileUtil.getTempDirectory() + "/lib";
    }

    public static boolean addToJavaLibraryPath(String directories[])
    {
        try
        {
            final String path_separator = System.getProperty("path.separator");

            // patch user library paths...
            final Field pathsField = ReflectionUtil.getField(ClassLoader.class, "usr_paths", true);
            // get current user paths
            final ArrayList<String> userPaths = CollectionUtil.asArrayList((String[]) pathsField.get(null));
            // get current system paths
            String sysPaths = System.getProperty("java.library.path");

            for (String dir : directories)
            {
                if (!userPaths.contains(dir))
                    userPaths.add(dir);
                if (!sysPaths.contains(dir))
                    sysPaths += path_separator + dir;
            }

            // set back user library path
            pathsField.set(null, userPaths.toArray(new String[userPaths.size()]));
            // set back system library path
            System.setProperty("java.library.path", sysPaths);

            return true;
        }
        catch (Exception e)
        {
            System.err.println(e.getMessage());
            System.err.println("Cannot patch Java Library Path.");

            return false;
        }
    }

    /**
     * Load the specified native library.
     * 
     * @param dir
     *        directory from where we want to load the native library.
     * @param name
     *        name of the library.<br/>
     *        The filename of the library is automatically built depending the operating system.
     */
    public static void loadLibrary(String dir, String name)
    {
        final File libPath = new File(dir, System.mapLibraryName(name));

        if (libPath.exists())
            System.load(libPath.getAbsolutePath());
        else
            System.loadLibrary(name);
    }

    /**
     * Load the specified native library.
     * 
     * @param pathname
     *        complete path or name of the library we want to load
     */
    public static void loadLibrary(String pathname)
    {
        final File file = new File(pathname);

        if (file.exists())
            System.load(file.getAbsolutePath());
        else
            System.loadLibrary(pathname);
    }
}