/*
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 * @copyright Ranides Atterwim
 * @license WTFPL
 * @url http://ranides.net/projects/assira
 */
package net.ranides.assira.system;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import lombok.experimental.UtilityClass;
import net.ranides.assira.generic.Ref;
import net.ranides.assira.generic.ValueUtils;
import net.ranides.assira.reflection.ResolveException;
import net.ranides.assira.text.FormatNumber;
import net.ranides.assira.text.ResolveDialect;
import net.ranides.assira.text.ResolveFormat;
import org.slf4j.LoggerFactory;

import java.security.AccessControlException;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.LinkedList;
import java.util.List;

/**
 * Utility class build on top of {@link Runtime} class.
 *
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 */
@UtilityClass
public class RuntimeUtils {

    private static final org.slf4j.Logger LOGGER = LoggerFactory.getLogger(RuntimeUtils.class);

    /**
     * Estimates memory used by application, in bytes.
     *
     * Please note, that JVM reserves substantially more memory from operating system than
     * it is using on currently allocated heap.
     *
     * @return long
     */
    public static long getMemoryUse() {
        Runtime runtime = Runtime.getRuntime();
        return runtime.totalMemory() - runtime.freeMemory();
    }
    
    
    /**
     * Runs the garbage collector. 
     * @return 
     *      Estimated amount of memory released by GC
     *      
     */
    @SuppressWarnings("PMD.DoNotCallGarbageCollectionExplicitly")
    @SuppressFBWarnings("DM_GC")
    public static long performGC() {
        long prev = getMemoryUse();
        Runtime.getRuntime().gc();
        return prev - getMemoryUse();
    }
    
    /**
     * Forces releasing {@code SoftReferences}, by causing OutOfMemoryError in VM.
     * @return 
     *      Estimated amount of memory released by GC
     */
    @SuppressWarnings({
        "PMD.DoNotCallGarbageCollectionExplicitly", 
        "MismatchedQueryAndUpdateOfCollection"
    })
    @SuppressFBWarnings("DM_GC")
    public static long forceGC() {
        long prev = Runtime.getRuntime().totalMemory();
        int block = (int)Math.min(Integer.MAX_VALUE, Runtime.getRuntime().freeMemory());
        try {
            final List<long[]> memhog = new LinkedList<>();
            while(true) {
                memhog.add(new long[block]);
            }
        }
        catch(final OutOfMemoryError e) {
            // at this point all SoftReferences have been released - GUARANTEED
            long alloc = Runtime.getRuntime().totalMemory() - prev;
            LOGGER.debug("Forced OutOfMemoryError by allocation {} bytes", FormatNumber.group(alloc));
            Runtime.getRuntime().gc();
            // at this point we should use minimal amount of memory
            return prev - getMemoryUse();
        }
    }

    /**
     * Uses {@link AccessController} to execute action with all permissions
     * possessed by the caller's protection domain.
     *
     * @param action action
     * @param <T> T
     * @return result of action
     */
    public static <T> T doPrivileged(PrivilegedAction<T> action) {
        return AccessController.doPrivileged(action);
    }

    /**
     * Uses {@link AccessController} to execute action with all permissions
     * possessed by the caller's protection domain.
     *
     * @param action action
     */
    public static void doPrivileged(Runnable action) {
        doPrivileged(()->{ action.run(); return null; });
    }

    /**
     * Reads system property in privileged mode.
     * Returns null if property does not exist.
     *
     * @param name name
     * @return value or null
     */
    public static Ref<String> getProperty(String name) {
        try {
            return Ref.ofNullable(doPrivileged(() -> System.getProperty(name)));
        } catch(AccessControlException $0) {
            return Ref.empty();
        }
    }

    /**
     * Reads system property in privileged mode.
     * Returns "other" if property does not exist.
     *
     * @param name name
     * @param other other
     * @return value or other
     */
    public static String getProperty(String name, String other) {
        try {
            return ValueUtils.or(doPrivileged(() -> System.getProperty(name)), other);
        } catch(AccessControlException $0) {
            return other;
        }
    }

    /**
     * Changes system property in privileged mode.
     *
     * @param name name
     * @param value value
     */
    public static void setProperty(String name, String value) {
        doPrivileged(() -> System.setProperty(name, value));
    }

    /**
     * Returns current JVM major version as int value.
     * For old JDK version using "1.X" scheme it returns "X", for example return 8 for JDK 1.8
     *
     * @return int
     */
    public static int getJavaVersion() {
        double version = Double.parseDouble(getProperty("java.specification.version", "0"));
        if(version < 2) {
            return (int)Math.round(10*version-10);
        } else {
            return (int)Math.round(version);
        }
    }

    /**
     * Expands all variable ${expressions} inside provided text, using "System.getenv".
     *
     * You can use more complex expressions supported by {@link ResolveDialect#SHELL}.
     *
     * Please note, that unknown variable will cause {@link ResolveException}
     *
     * @param value value
     * @return String
     * @throws ResolveException if some variable does not exist
     */
    public static String expand(String value) throws ResolveException {
        return ResolveFormat.compile(ResolveDialect.SHELL, value).format(System.getenv());
    }

}
