// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.

package com.jetbrains.cef;

import com.jetbrains.cef.remote.NativeServerManager;
import org.cef.CefApp;
import org.cef.CefSettings;
import org.cef.OS;
import org.cef.SystemBootstrap;
import org.cef.misc.Utils;

import java.awt.*;
import java.io.*;
import java.util.*;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;

/**
 * @author Anton Tarasov
 * TODO: Why it's abstact? Check if removing abstract can be promoted
 */
public abstract class JCefAppConfig {
    protected final CefSettings cefSettings = new CefSettings();
    protected final List<String> appArgs = new ArrayList<>();
    protected SystemBootstrap.Loader loader = null;
    private String cefFrameworkPathOSX = null;
    private static final AtomicReference<Double> forceDeviceScaleFactor = new AtomicReference<>(Double.valueOf(0));
    private String nativeBundlePath = null;

    public String[] getAppArgs() {
        return appArgs.toArray(new String[0]);
    }

    public List<String> getAppArgsAsList() {
        return appArgs;
    }

    public CefSettings getCefSettings() {
        return cefSettings;
    }

    public String getCefFrameworkPathOSX() {
        return cefFrameworkPathOSX;
    }

    public String getNativeBundlePath() {
        return nativeBundlePath;
    }

    public SystemBootstrap.Loader getLoader() {
        return loader;
    }

    public static JCefAppConfig getInstance(String nativeBundlePath) {
        return getInstance(nativeBundlePath, false);
    }

    public static JCefAppConfig getInstance(String nativeBundlePath, boolean outOfProcess) {
        JCefAppConfig appConfig = new JCefAppConfig() {
        };
        appConfig.nativeBundlePath = nativeBundlePath;
        if (OS.isMacintosh()) {
            appConfig.cefFrameworkPathOSX = Utils.pathOf(nativeBundlePath, "Frameworks/Chromium Embedded Framework.framework");
            if (!outOfProcess) {
                appConfig.appArgs.add("--framework-dir-path=" + appConfig.cefFrameworkPathOSX);
                appConfig.appArgs.add("--main-bundle-path=" + Utils.pathOf(nativeBundlePath, "Frameworks/jcef Helper.app"));
                appConfig.appArgs.add("--browser-subprocess-path=" + Utils.pathOf(nativeBundlePath, "Frameworks/jcef Helper.app/Contents/MacOS/jcef Helper"));
            }

            appConfig.appArgs.add("--disable-in-process-stack-traces");
            appConfig.appArgs.add("--use-mock-keychain");
            appConfig.appArgs.add("--disable-features=SpareRendererForSitePerProcess");

            appConfig.appArgs.add("--disable-notifications");
            //  NOTE: some other switches are also could be usefull:
            //    "suppress-message-center-popups"
            //    "disable-infobars"
            //    "disable-notifications"
            //    "disable-push-api"
            //    "disable-default-apps"
            //    "deny-permission-prompts"
            appConfig.loader = new SystemBootstrap.Loader() {
                @Override
                public void loadLibrary(String libname) {
                    libname = Utils.pathOf(nativeBundlePath, "lib" + libname + ".dylib");
                    System.load(libname);
                }
            };
        } else if (OS.isLinux()) {
            if (!outOfProcess) {
                appConfig.cefSettings.browser_subprocess_path = Utils.pathOf(nativeBundlePath, "jcef_helper");
            }
            appConfig.cefSettings.resources_dir_path = Utils.pathOf(nativeBundlePath);
            appConfig.cefSettings.locales_dir_path = Utils.pathOf(nativeBundlePath, "locales");
            appConfig.loader = new SystemBootstrap.Loader() {
                @Override
                public void loadLibrary(String libname) {
                    libname = Utils.pathOf(nativeBundlePath, "lib" + libname + ".so");
                    System.load(libname);
                }
            };
        } else if (OS.isWindows()) {
            if (!outOfProcess) {
                appConfig.cefSettings.browser_subprocess_path = Utils.pathOf(nativeBundlePath, "jcef_helper.exe");
            }

            appConfig.loader = new SystemBootstrap.Loader() {
                final private Set<String> bundledLibs = new HashSet<>(Arrays.asList("chrome_elf", "jcef", "libcef", "shared_mem_helper"));
                @Override
                public void loadLibrary(String libname) {
                    if (bundledLibs.contains(libname)) {
                        System.load(Utils.pathOf(nativeBundlePath, libname + ".dll"));
                    } else {
                        System.loadLibrary(libname);
                    }
                }
            };

        } else {
            throw new IllegalStateException("JCEF is not supported on this platform: " + System.getProperty("os.name", "unknown"));
        }

        return appConfig;
    }

    /**
     * @throws IllegalStateException in case of unsupported platform
     */
    public static JCefAppConfig getInstance() {
        JCefAppConfig appConfig = new JCefAppConfig() {
        };
        if (OS.isMacintosh()) {
            String javaRoot = Utils.pathOf(System.getProperty("java.home"), "/..");
            String frameworkPath = Utils.pathOf(javaRoot, "/Frameworks/Chromium Embedded Framework.framework");
            String cefHelperPath = Utils.pathOf(javaRoot, "/Frameworks/jcef Helper.app");
            String subprocessPath = Utils.pathOf(cefHelperPath, "/Contents/MacOS/jcef Helper");

            appConfig.appArgs.add("--framework-dir-path=" + frameworkPath);
            appConfig.appArgs.add("--browser-subprocess-path=" + subprocessPath);
            appConfig.appArgs.add("--main-bundle-path=" + cefHelperPath);

            appConfig.appArgs.add("--disable-in-process-stack-traces");
            appConfig.appArgs.add("--use-mock-keychain");
            appConfig.appArgs.add("--disable-features=SpareRendererForSitePerProcess");

            appConfig.appArgs.add("--disable-notifications");
        } else if (OS.isLinux()) {
            String libPath = Utils.pathOf(System.getProperty("java.home"), "lib");
            appConfig.cefSettings.resources_dir_path = libPath;
            appConfig.cefSettings.locales_dir_path = Utils.pathOf(libPath, "locales");
            appConfig.cefSettings.browser_subprocess_path = Utils.pathOf(libPath, "jcef_helper");

            double scale = getDeviceScaleFactor(null);
            appConfig.appArgs.add("--force-device-scale-factor=" + scale);
            appConfig.appArgs.add("--disable-features=SpareRendererForSitePerProcess");
        } else if (OS.isWindows()) {
            String binPath = System.getProperty("java.home") + "/bin";
            String libPath = System.getProperty("java.home") + "/lib";
            appConfig.cefSettings.browser_subprocess_path = binPath + "/jcef_helper.exe";

            appConfig.appArgs.add("--disable-features=SpareRendererForSitePerProcess");
        } else {
            throw new IllegalStateException("JCEF is not supported on this platform: " + System.getProperty("os.name", "unknown"));
        }

        appConfig.loader = new SystemBootstrap.Loader() {
            @Override
            public void loadLibrary(String libname) {
                System.loadLibrary(libname);
            }
        };

        return appConfig;
    }

    /**
     * @deprecated don't use. This function is temporary here.
     */
    @Deprecated
    public static String getJbrFrameworkPathOSX() {
        if (OS.isMacintosh()) {
            return Utils.pathOf(System.getProperty("java.home"), "../Frameworks/Chromium Embedded Framework.framework");
        }
        return null;
    }

    /**
     * Tries to load full JCEF version string from version.info file
     */
    private static String getVersionEx() throws IOException {
        try (InputStream inputStream = JCefAppConfig.class.getResourceAsStream("version.info")) {
            return new BufferedReader(new InputStreamReader(inputStream)).readLine();
        }
    }

    /**
     * Returns the full version string before {@link org.cef.CefApp} is created.
     * Otherwise use {@link CefApp#getVersion()}.
     */
    public static String getVersion() {
        try {
            return getVersionEx();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * Returns runtime {@link JCefVersionDetails JCEF version details}
     */
    public static JCefVersionDetails getVersionDetails() throws JCefVersionDetails.VersionUnavailableException {
        try {
            return new JCefVersionDetails(getVersionEx());
        } catch (IOException e) {
            throw new JCefVersionDetails.VersionUnavailableException("Unable to load version information", e);
        }
    }


    public static double getDeviceScaleFactor(/*@Nullable*/Component component) {
        if (GraphicsEnvironment.isHeadless()) {
            return 1.0;
        }
        double scale = getForceDeviceScaleFactor();
        if (scale > 0) return scale;

        GraphicsDevice device = null;
        try {
            if (component != null && component.getGraphicsConfiguration() != null) {
                device = component.getGraphicsConfiguration().getDevice();
            } else {
                device = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice();
            }
        } catch (Throwable t) {
            t.printStackTrace();
        }
        if (device != null) {
            GraphicsConfiguration gc = device.getDefaultConfiguration();
            if (gc != null) {
                scale = gc.getDefaultTransform().getScaleX();
            }
        }
        return scale;
    }

    /**
     * Defined to support IDE-managed HiDPI mode in IDEA, undefined in JRE-managed HiDPI.
     */
    @Deprecated
    public static double getForceDeviceScaleFactor() {
        if (forceDeviceScaleFactor.get() == 0) {
            synchronized (forceDeviceScaleFactor) {
                String prop = System.getProperty("jcef.forceDeviceScaleFactor");
                if (prop != null) {
                    try {
                        forceDeviceScaleFactor.set(Double.parseDouble(prop));
                    } catch (NumberFormatException e) {
                        forceDeviceScaleFactor.set(Double.valueOf(-1));
                        e.printStackTrace();
                    }
                } else {
                    forceDeviceScaleFactor.set(Double.valueOf(-1));
                }
            }
        }
        return forceDeviceScaleFactor.get();
    }

    public boolean isRemoteSupported() {
        return nativeBundlePath != null || NativeServerManager.isRemoteSupported();
    }

    public boolean isRemoteEnabled() {
        return isRemoteSupported() && Boolean.getBoolean("jcef.remote.enabled");
    }

    public File getServerExe() {
        if (nativeBundlePath == null) {
            return null;
        } else if (OS.isMacintosh()) {
            return new File(nativeBundlePath, "Frameworks/cef_server.app/Contents/MacOS/cef_server");
        } else if (OS.isLinux()) {
            return new File(nativeBundlePath, "cef_server");
        } else if (OS.isWindows()) {
            return new File(nativeBundlePath, "cef_server.exe");
        }

        throw new IllegalStateException("Unsupported platform: " + System.getProperty("os.name", "unknown"));
    }
}
