/*
 * Copyright (c) 2007, 2011, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code 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
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package com.sun.tools.visualvm.profiler;

import com.sun.tools.visualvm.application.Application;
import com.sun.tools.visualvm.application.jvm.Jvm;
import com.sun.tools.visualvm.application.jvm.JvmFactory;
import com.sun.tools.visualvm.core.datasource.descriptor.DataSourceDescriptor;
import com.sun.tools.visualvm.core.datasource.descriptor.DataSourceDescriptorFactory;
import com.sun.tools.visualvm.core.datasupport.DataChangeEvent;
import com.sun.tools.visualvm.core.datasupport.DataChangeListener;
import com.sun.tools.visualvm.core.datasupport.Stateful;
import com.sun.tools.visualvm.core.ui.DataSourceView;
import com.sun.tools.visualvm.core.ui.DataSourceWindowManager;
import com.sun.tools.visualvm.host.Host;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import javax.swing.SwingUtilities;
import org.netbeans.lib.profiler.common.Profiler;
import org.netbeans.lib.profiler.common.ProfilingSettings;
import org.netbeans.lib.profiler.common.SessionSettings;
import org.netbeans.lib.profiler.global.Platform;
import org.netbeans.modules.profiler.NetBeansProfiler;
import org.netbeans.modules.profiler.ProfilerControlPanel2;
import org.netbeans.modules.profiler.api.ProfilerIDESettings;
import org.openide.DialogDisplayer;
import org.openide.NotifyDescriptor;
import org.openide.util.NbBundle;
import org.openide.util.RequestProcessor;

/**
 *
 * @author Jiri Sedlacek
 */
public final class ProfilerSupport {
    
//    private static final Logger LOGGER = Logger.getLogger(ProfilerSupport.class.getName());
    
    private static final boolean FORCE_PROFILING_SUPPORTED =
            Boolean.getBoolean("com.sun.tools.visualvm.profiler.SupportAllVMs");   // NOI18N
    private static final String HOTSPOT_VM_NAME_PREFIX = "Java HotSpot";    // NOI18N
    private static final String OPENJDK_VM_NAME_PREFIX = "OpenJDK ";    // NOI18N
    private static final String SUN_VM_VENDOR_PREFIX = "Sun ";  // NOI18N
    private static final String ORACLE_VM_VENDOR_PREFIX = "Oracle ";  // NOI18N
    private static final String APPLE_VM_VENDOR_PREFIX = "Apple ";  // NOI18N
    private static final String HP_VM_VENDOR_PREFIX = "\"Hewlett-Packard "; // NOI18N
    
    private static final String JAVA_RT_16_PREFIX = "1.6.0";  // NOI18N
    private static final String JAVA_RT_17_PREFIX = "1.7.0";  // NOI18N
    private static final String JAVA_RT_18_PREFIX = "1.8.0";  // NOI18N
    private static final String JAVA_RT_19_PREFIX = "1.9.0";  // NOI18N
    
    private static ProfilerSupport instance;
    
    private boolean isInitialized;
    
    private Application profiledApplication;
    private final ApplicationProfilerViewProvider profilerViewProvider;


    public static synchronized ProfilerSupport getInstance() {
        if (instance == null) instance = new ProfilerSupport();
        return instance;
    }
    
    
    boolean isInitialized() {
        return isInitialized;
    }
    
    public String getProfiledApplicationName() {
        String name = NbBundle.getMessage(ProfilerSupport.class, "STR_Externaly_started_app"); // NOI18N
        Application a = getProfiledApplication();
        if (a == null) {
            int state = NetBeansProfiler.getDefaultNB().getProfilingState();
            return state == NetBeansProfiler.PROFILING_INACTIVE ? null : name;
        }
        DataSourceDescriptor d = DataSourceDescriptorFactory.getDescriptor(a);
        return d != null ? d.getName() : name;
    }
    
    public int getDefaultPort() {
        return ProfilerIDESettings.getInstance().getPortNo();
    }
    
    public boolean hasSupportedJavaPlatforms() {
        for (int i = 0; i < 5; i++) {
            String code = "jdk1" + (5 + i); // NOI18N
            if (supportsProfiling(code, 32) || supportsProfiling(code, 64)) return true;
        }
        return false;
    }
    
    public String[][] getSupportedJavaPlatforms() {
        List<String> codesl = new ArrayList();
        
        for (int i = 0; i < 5; i++) {
            String code = "jdk1" + (5 + i); // NOI18N
            if (supportsProfiling(code, 32) || supportsProfiling(code, 64)) codesl.add(code);
        }
        
        String[] names = new String[codesl.size()];
        String[] codes = new String[codesl.size()];
        String current = null;
        for (int i = 0; i < codesl.size(); i++) {
            codes[i] = codesl.get(i);
            names[i] = getJavaName(codes[i]);
            if (Platform.getJDKVersionString().equals(codes[i])) current = names[i];
        }
        
        return new String[][] { names, codes, { current } };
    }
    
    public String[][] getSupportedArchitectures(String java) {
        List<String> codesl = new ArrayList();
        
        if (supportsProfiling(java, 32)) codesl.add(Integer.toString(32));
        if (supportsProfiling(java, 64)) codesl.add(Integer.toString(64));
        
        String[] names = new String[codesl.size()];
        String[] codes = new String[codesl.size()];
        String current = null;
        for (int i = 0; i < codesl.size(); i++) {
            codes[i] = codesl.get(i);
            names[i] = getArchName(Integer.parseInt(codes[i]));
            if (Integer.toString(Platform.getSystemArchitecture()).equals(codes[i])) current = names[i];
        }
        
        return new String[][] { names, codes, { current } };
    }
    
    static String getJavaName(String code) {
        if (Platform.JDK_15_STRING.equals(code))
            return NbBundle.getMessage(ProfilerSupport.class, "STR_Java_platform_name", 5); // NOI18N
        if (Platform.JDK_16_STRING.equals(code))
            return NbBundle.getMessage(ProfilerSupport.class, "STR_Java_platform_name", 6); // NOI18N
        if (Platform.JDK_17_STRING.equals(code))
            return NbBundle.getMessage(ProfilerSupport.class, "STR_Java_platform_name", 7); // NOI18N
        if (Platform.JDK_18_STRING.equals(code))
            return NbBundle.getMessage(ProfilerSupport.class, "STR_Java_platform_name", 8); // NOI18N
        if (Platform.JDK_19_STRING.equals(code))
            return NbBundle.getMessage(ProfilerSupport.class, "STR_Java_platform_name", 9); // NOI18N
        throw new IllegalArgumentException("Unknown java code " + code); // NOI18N
    }
    
    static String getArchName(int arch) {
        if (32 == arch) return NbBundle.getMessage(ProfilerSupport.class, "STR_Java_arch_name", 32); // NOI18N
        if (64 == arch) return NbBundle.getMessage(ProfilerSupport.class, "STR_Java_arch_name", 64); // NOI18N
        throw new IllegalArgumentException("Unsupported architecture " + arch); // NOI18N
    }
    
    public boolean supportsProfiling(String java, int architecture) {
        String ld = Profiler.getDefault().getLibsDir();
        String nativeLib = Platform.getAgentNativeLibFullName(ld, false, java, architecture);
        return new File(nativeLib).isFile();
    }
    
    public String getStartupParameter(String java, int architecture, int port) {
        String ld = Profiler.getDefault().getLibsDir();
        if (ld.contains(" ")) ld = "\"" + ld + "\""; // NOI18N
        
        String nativeLib = Platform.getAgentNativeLibFullName(ld, false, java, architecture);        
        if (nativeLib.contains(" ")) nativeLib = "\"" + nativeLib + "\""; // NOI18N
        
        return "-agentpath:" + nativeLib + "=" + ld + "," + port; // NOI18N
    }
    
    public void profileProcessStartup(final String java, final int architecture, final int port,
                                      final CPUSettingsSupport cpuSettings,
                                      final MemorySettingsSupport memorySettings,
                                      final boolean isCPUProfiling) {
        
        if (!CalibrationSupport.checkCalibration(java, architecture, null, null)) return;
        
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                // prevent deadlock, similar to issue #570
                ProfilerControlPanel2.getDefault();
                
                // Perform the actual attach
                RequestProcessor.getDefault().post(new Runnable() {
                    public void run() {
                        if (!checkStartedApp(port)) return;
                        
                        final RequestProcessor processor = new RequestProcessor("Startup Profiler @ " + port); // NOI18N
                        Host.LOCALHOST.getRepository().addDataChangeListener(
                            new DataChangeListener<Application>() {
                                public void dataChanged(final DataChangeEvent<Application> event) {
                                    final DataChangeListener listener = this;
                                    processor.post(new Runnable() {
                                        public void run() {
                                            if (!event.getAdded().equals(event.getCurrent())) // filter-out initial sync event
                                                for (Application a : event.getAdded()) {
                                                    if (isProfiledApplication(a, port)) {
                                                        Host.LOCALHOST.getRepository().removeDataChangeListener(listener);

                                                        setProfiledApplication(a);
                                                        selectProfilerView(a, cpuSettings, memorySettings);

                                                        break;
                                                    }
                                                }
                                        }
                                    });
                                }
                            }, Application.class);

                        ProfilingSettings ps = createProfilingSettings(cpuSettings, memorySettings, isCPUProfiling);
                        SessionSettings ss = createSessionSettings(java, architecture, port);
                        NetBeansProfiler.getDefaultNB().connectToStartedApp(ps, ss);
                        resetTerminateDialogs();
                    }
                });
            }
        });
    }
    
    private static boolean checkStartedApp(int port) {
        String homeDir = System.getProperty("user.home"); // NOI18N
        File agentF = new File(homeDir + File.separator + ".nbprofiler" + File.separator + port); // NOI18N
        if (!agentF.isFile()) return true;
        
        String caption = NbBundle.getMessage(ProfilerSupport.class, "CAP_Warning"); // NOI18N
        String message = NbBundle.getMessage(ProfilerSupport.class, "MSG_StartedTooSoon"); // NOI18N
        NotifyDescriptor nd = new NotifyDescriptor(message, caption, NotifyDescriptor.OK_CANCEL_OPTION,
                                  NotifyDescriptor.WARNING_MESSAGE, null, NotifyDescriptor.OK_OPTION);
        if (DialogDisplayer.getDefault().notify(nd) != NotifyDescriptor.OK_OPTION) return false;
        return checkStartedApp(port);
    }
    
    private static void resetTerminateDialogs() {
        String dnsaKey = "NetBeansProfiler.handleShutdown.noResults"; // NOI18N
        ProfilerIDESettings.getInstance().setDoNotShowAgain(dnsaKey, null);
        dnsaKey = "NetBeansProfiler.handleShutdown"; // NOI18N
        String dnsa = ProfilerIDESettings.getInstance().getDoNotShowAgain(dnsaKey);
        if ("NO_OPTION".equals(dnsa)) ProfilerIDESettings.getInstance().setDoNotShowAgain(dnsaKey, null); // NOI18N        }
    }
    
    private static ProfilingSettings createProfilingSettings(CPUSettingsSupport cpuSettings,
            MemorySettingsSupport memorySettings, boolean isCPUProfiling) {
        
        return isCPUProfiling ? cpuSettings.getSettings() : memorySettings.getSettings();
    }
    
    private static SessionSettings createSessionSettings(String java, int architecture, int port) {
        SessionSettings ss = new SessionSettings();
        ss.setJavaVersionString(java);
        ss.setSystemArchitecture(architecture);
        ss.setPortNo(port);
        ss.setJavaExecutable(JavaInfo.getCurrentJDKExecutable()); // Workaround for calibration check, not used for profiling
        return ss;
    }
    
    private static boolean isProfiledApplication(Application a, int port) {
        Jvm jvm = JvmFactory.getJVMFor(a);
        if (!jvm.isBasicInfoSupported()) return false;
        String args = jvm.getJvmArgs();
        return args.contains("-agentpath:") && args.contains("," + port); // NOI18N
    }
    
    
    boolean supportsProfiling(Application application) {
        // Application already being profiled (Startup Profiler)
        if (application == getProfiledApplication()) return true;
        
        // Remote profiling is not supported
        if (application.getHost() != Host.LOCALHOST) return false;
        
        // Profiling current VisualVM instance is not supported
        if (Application.CURRENT_APPLICATION.equals(application)) return false;
        
        // Profiled application has to be running
        if (application.getState() != Stateful.STATE_AVAILABLE) return false;
        
        
        Jvm jvm = JvmFactory.getJVMFor(application);
        
        // Basic info has to be supported and VM has to be attachable
        if (!jvm.isBasicInfoSupported() || !jvm.isAttachable()) return false;
        
        // User explicitly requests to profile any VM
        if (FORCE_PROFILING_SUPPORTED) return true;
        
        // Profiled application needs to be running JDK 6.0 or 7.0 or 8.0 or 9.0
        if (!jvm.is16() && !jvm.is17() && !jvm.is18() && !jvm.is19()) return false;
        
        String vmName = jvm.getVmName();
        String vmVendor = jvm.getVmVendor();
        
        // VM has to be a HotSpot VM or OpenJDK by Sun Microsystems Inc. or Oracle Co. or Apple Inc. or Hewlett-Packard Co.
        return vmName != null && (vmName.startsWith(HOTSPOT_VM_NAME_PREFIX) || vmName.startsWith(OPENJDK_VM_NAME_PREFIX)) && 
               vmVendor != null && (vmVendor.startsWith(ORACLE_VM_VENDOR_PREFIX) || vmVendor.startsWith(SUN_VM_VENDOR_PREFIX) 
                                 || vmVendor.startsWith(APPLE_VM_VENDOR_PREFIX) || vmVendor.startsWith(HP_VM_VENDOR_PREFIX));
    }
    
    static boolean classSharingBreaksProfiling(Application application) {
        if (application.getState() != Stateful.STATE_AVAILABLE) return false;

        Jvm jvm = JvmFactory.getJVMFor(application);
        String vmInfo = jvm.getVmInfo();
        boolean classSharing = vmInfo.contains("sharing"); // NOI18N
        
        if (!jvm.isGetSystemPropertiesSupported()) return classSharing;
        Properties properties = jvm.getSystemProperties();
        if (properties == null) return classSharing;
        
        String javaRTVersion = properties.getProperty("java.runtime.version"); // NOI18N
        if (javaRTVersion == null) return classSharing;
        
        int updateNumber = getUpdateNumber(javaRTVersion);
        int buildNumber = getBuildNumber(javaRTVersion);
        String vmName = jvm.getVmName();
        
        // Sun JDK & derived JDKs ----------------------------------------------
        if (vmName.startsWith(HOTSPOT_VM_NAME_PREFIX)) {
            // JDK 6.0 is OK from Update 6 except of Update 10 Build 23 and lower
            if (javaRTVersion.startsWith(JAVA_RT_16_PREFIX)) {
                if (updateNumber == 10) {
                    if (buildNumber >= 24) return false;
                } else if (updateNumber >= 6) return false;
            // JDK 7.0 is OK from Build 26
            } else if (javaRTVersion.startsWith(JAVA_RT_17_PREFIX)) {
                if (updateNumber == 0) {
                    if (buildNumber >= 26) return false;
                } else {
                    return false;
                }
            } else if (javaRTVersion.startsWith(JAVA_RT_18_PREFIX)) {
                return false;
            } else if (javaRTVersion.startsWith(JAVA_RT_19_PREFIX)) {
                return false;
            }
        // OpenJDK -------------------------------------------------------------
        } else if(vmName.startsWith(OPENJDK_VM_NAME_PREFIX)) {
            // OpenJDK 6 is OK from Build 11
            if (javaRTVersion.startsWith(JAVA_RT_16_PREFIX)) {
                if (updateNumber == 0) {
                    if (buildNumber >= 11) return false;
                } else {
                    return false;
                }
            // OpenJDK 7 is assumed to be OK from Build 26 (not tested)
            } else if (javaRTVersion.startsWith(JAVA_RT_17_PREFIX)) {
                if (updateNumber == 0) {
                    if (buildNumber >= 26) return false;
                } else {
                    return false;
                }
            }
            // OpenJDK 8 should be OK
            else if (javaRTVersion.startsWith(JAVA_RT_18_PREFIX)) {
                return false;
            }
            else if (javaRTVersion.startsWith(JAVA_RT_19_PREFIX)) {
                return false;
            }
        }
        
        return classSharing;
    }
    
    private static int getUpdateNumber(String javaRTVersion) {
        int underscoreIndex = javaRTVersion.indexOf("_"); // NOI18N
        if (underscoreIndex == -1) return 0; // Assumes no update, may be incorrect for unexpected javaRTVersion format
        
        try {
            String updateNumberString = javaRTVersion.substring(underscoreIndex + "_".length(), javaRTVersion.indexOf("-")); // NOI18N
            return Integer.parseInt(updateNumberString);
        } catch (Exception e) {}
        
        return -1;
    }
    
    private static int getBuildNumber(String javaRTVersion) {
        try {
            String buildNumberString = javaRTVersion.substring(javaRTVersion.indexOf("-b") + "-b".length()); // NOI18N
            return Integer.parseInt(buildNumberString);
        } catch (Exception e) {}
        
        return -1;
    }
    
    synchronized void setProfiledApplication(Application profiledApplication) {
        this.profiledApplication = profiledApplication;
    }
  
    synchronized Application getProfiledApplication() {
        return profiledApplication;
    }
    
    void selectActiveProfilerView() {
        selectProfilerView(getProfiledApplication());
    }
    
    void selectProfilerView(Application application) {
        selectProfilerView(application, null, null);
    }
    
    private void selectProfilerView(Application application, final CPUSettingsSupport cpu, final MemorySettingsSupport memory) {
        if (application == null) return;
        final DataSourceView activeView = profilerViewProvider.view(application);
        if (activeView == null) return;
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                if (cpu != null || memory != null) ((ApplicationProfilerView)activeView).copySettings(cpu, memory);
                DataSourceWindowManager.sharedInstance().selectView(activeView);
            }
        });
    }
    
    
    private ProfilerSupport() {
        profilerViewProvider = new ApplicationProfilerViewProvider();
        profilerViewProvider.initialize();

        ProfilerIDESettings.getInstance().setAutoOpenSnapshot(false);
        ProfilerIDESettings.getInstance().setAutoSaveSnapshot(true);
    }

}
