package com.github.games647.lagmonitor;

import com.sun.management.UnixOperatingSystemMXBean;

import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.lang.management.OperatingSystemMXBean;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.FileStore;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Optional;
import java.util.logging.Level;
import java.util.logging.Logger;

import oshi.SystemInfo;
import oshi.hardware.GlobalMemory;
import oshi.software.os.OSProcess;

public class NativeManager {

    private static final String JNA_FILE = "jna-5.5.0.jar";

    private final Logger logger;
    private final Path dataFolder;

    private final OperatingSystemMXBean osBean = ManagementFactory.getOperatingSystemMXBean();
    private SystemInfo info;

    private int pid = -1;

    public NativeManager(Logger logger, Path dataFolder) {
        this.logger = logger;
        this.dataFolder = dataFolder;
    }

    public void setupNativeAdapter() {
        try {
            if (!loadExternalJNI()) {
                if (!(osBean instanceof com.sun.management.OperatingSystemMXBean)) {
                    logger.severe("You're not using Oracle Java nor using the native library. " +
                            "You won't be able to read some native data");
                }

                return;
            }
        } catch (IOException | ReflectiveOperationException ex) {
            logger.log(Level.WARNING, "Cannot load JNA library. We continue without it", ex);
            return;
        }

        logger.info("Found JNA native library. Enabling extended native data support to display more data");
        try {
            info = new SystemInfo();

            //make a test call
            pid = info.getOperatingSystem().getProcessId();
        } catch (UnsatisfiedLinkError | NoClassDefFoundError linkError) {
            logger.log(Level.INFO, "Cannot load native library. Continuing without it...", linkError);
            info = null;
        }
    }

    private boolean loadExternalJNI() throws IOException, ReflectiveOperationException {
        Path jnaPath = dataFolder.resolve(JNA_FILE);
        if (Files.exists(jnaPath)) {
            extractJNI(jnaPath);
            return true;
        } else {
            logger.info("JNA not found. " +
                    "Please download the this to the folder of this plugin to display more data about your setup");
            logger.info("https://repo1.maven.org/maven2/net/java/dev/jna/jna/5.5.0/jna-5.5.0.jar");
        }

        return false;
    }

    public Optional<SystemInfo> getSystemInfo() {
        return Optional.ofNullable(info);
    }

    private void extractJNI(Path jnaPath) throws IOException, ReflectiveOperationException {
        Method addUrlMethod = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
        addUrlMethod.setAccessible(true);

        ClassLoader classLoader = getClass().getClassLoader();
        addUrlMethod.invoke(classLoader, jnaPath.toUri().toURL());

        String libName = "/com/sun/jna/" + com.sun.jna.Platform.RESOURCE_PREFIX
                + '/' + System.mapLibraryName("jnidispatch").replace(".dylib", ".jnilib");
        com.sun.jna.Native.extractFromResourcePath(libName, classLoader);
    }

    public double getProcessCPULoad() {
        if (osBean instanceof com.sun.management.OperatingSystemMXBean) {
            com.sun.management.OperatingSystemMXBean nativeOsBean = (com.sun.management.OperatingSystemMXBean) osBean;
            return nativeOsBean.getProcessCpuLoad();
        }

        return -1;
    }

    public Optional<OSProcess> getProcess() {
        if (info == null) {
            return Optional.empty();
        }

        return Optional.of(info.getOperatingSystem().getProcess(pid));
    }

    public double getCPULoad() {
        if (osBean instanceof com.sun.management.OperatingSystemMXBean) {
            com.sun.management.OperatingSystemMXBean nativeOsBean = (com.sun.management.OperatingSystemMXBean) osBean;
            return nativeOsBean.getSystemCpuLoad();
        } else if (info != null) {
            return info.getHardware().getProcessor().getSystemLoadAverage(1)[0];
        }

        return -1;
    }

    public long getOpenFileDescriptors() {
        if (osBean instanceof com.sun.management.UnixOperatingSystemMXBean) {
            return ((UnixOperatingSystemMXBean) osBean).getOpenFileDescriptorCount();
        } else if (info != null) {
            return info.getOperatingSystem().getFileSystem().getOpenFileDescriptors();
        }

        return -1;
    }

    public long getMaxFileDescriptors() {
        if (osBean instanceof com.sun.management.UnixOperatingSystemMXBean) {
            return ((UnixOperatingSystemMXBean) osBean).getMaxFileDescriptorCount();
        } else if (info != null) {
            return info.getOperatingSystem().getFileSystem().getMaxFileDescriptors();
        }

        return -1;
    }

    public long getTotalMemory() {
        if (osBean instanceof com.sun.management.OperatingSystemMXBean) {
            com.sun.management.OperatingSystemMXBean nativeOsBean = (com.sun.management.OperatingSystemMXBean) osBean;
            return nativeOsBean.getTotalPhysicalMemorySize();
        } else if (info != null) {
            return info.getHardware().getMemory().getTotal();
        }

        return -1;
    }

    public long getFreeMemory() {
        if (osBean instanceof com.sun.management.OperatingSystemMXBean) {
            com.sun.management.OperatingSystemMXBean nativeOsBean = (com.sun.management.OperatingSystemMXBean) osBean;
            return nativeOsBean.getFreePhysicalMemorySize();
        } else if (info != null) {
            return getTotalMemory() - info.getHardware().getMemory().getAvailable();
        }

        return -1;
    }

    public long getFreeSwap() {
        if (osBean instanceof com.sun.management.OperatingSystemMXBean) {
            com.sun.management.OperatingSystemMXBean nativeOsBean = (com.sun.management.OperatingSystemMXBean) osBean;
            return nativeOsBean.getFreeSwapSpaceSize();
        } else if (info != null) {
            GlobalMemory memory = info.getHardware().getMemory();
            return memory.getAvailable();
        }

        return -1;
    }

    public long getTotalSwap() {
        if (osBean instanceof com.sun.management.OperatingSystemMXBean) {
            com.sun.management.OperatingSystemMXBean nativeOsBean = (com.sun.management.OperatingSystemMXBean) osBean;
            return nativeOsBean.getTotalSwapSpaceSize();
        } else if (info != null) {
            return info.getHardware().getMemory().getVirtualMemory().getSwapTotal();
        }

        return -1;
    }

    public long getFreeSpace() {
        long freeSpace = 0;
        try {
            FileStore fileStore = Files.getFileStore(Paths.get("."));
            freeSpace = fileStore.getUsableSpace();
        } catch (IOException ioEx) {
            logger.log(Level.WARNING, "Cannot calculate free/total disk space", ioEx);
        }

        return freeSpace;
    }

    public long getTotalSpace() {
        long totalSpace = 0;
        try {
            FileStore fileStore = Files.getFileStore(Paths.get("."));
            totalSpace = fileStore.getTotalSpace();
        } catch (IOException ioEx) {
            logger.log(Level.WARNING, "Cannot calculate free disk space", ioEx);
        }

        return totalSpace;
    }
}