/**
 * Copyright 2010-2023 JogAmp Community. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, are
 * permitted provided that the following conditions are met:
 *
 *    1. Redistributions of source code must retain the above copyright notice, this list of
 *       conditions and the following disclaimer.
 *
 *    2. Redistributions in binary form must reproduce the above copyright notice, this list
 *       of conditions and the following disclaimer in the documentation and/or other materials
 *       provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``AS IS'' AND ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JogAmp Community OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * The views and conclusions contained in the software and documentation are those of the
 * authors and should not be interpreted as representing official policies, either expressed
 * or implied, of JogAmp Community.
 */

package com.jogamp.common.os;

import java.security.PrivilegedAction;
import java.util.concurrent.TimeUnit;

import com.jogamp.common.jvm.JNILibLoaderBase;
import com.jogamp.common.net.Uri;
import com.jogamp.common.util.JarUtil;
import com.jogamp.common.util.PropertyAccess;
import com.jogamp.common.util.ReflectionUtil;
import com.jogamp.common.util.SecurityUtil;
import com.jogamp.common.util.VersionNumber;
import com.jogamp.common.util.cache.TempJarCache;

import jogamp.common.jvm.JVMUtil;
import jogamp.common.os.MachineDataInfoRuntime;
import jogamp.common.os.PlatformPropsImpl;

/**
 * Utility class for querying platform specific properties.
 * <p>
 * Some field declarations and it's static initialization has been delegated
 * to it's super class {@link PlatformPropsImpl} to solve
 * static initialization interdependencies w/ the GlueGen native library loading
 * and it's derived information {@link #getMachineDataInfo()}, {@link #is32Bit()}, ..<br>
 * This mechanism is preferred in this case to avoid synchronization and locking
 * and allow better performance accessing the mentioned fields/methods.
 * </p>
 */
public class Platform extends PlatformPropsImpl {

    public enum OSType {
        LINUX, FREEBSD, ANDROID, MACOS, SUNOS, HPUX, WINDOWS, OPENKODE, IOS;
    }

    public enum CPUFamily {
        /** AMD/Intel */
        X86,
        /** ARM */
        ARM,
        /** Power PC */
        PPC,
        /** SPARC */
        SPARC,
        /** Mips */
        MIPS,
        /** PA RISC */
        PA_RISC,
        /** Itanium */
        IA64,
        /** Hitachi SuperH */
        SuperH;
    }

    public enum CPUType {
        /** ARM 32bit default, usually little endian */
        ARM(       CPUFamily.ARM,     true),
        /** ARM7EJ, ARM9E, ARM10E, XScale, usually little endian */
        ARMv5(     CPUFamily.ARM,     true),
        /** ARM11, usually little endian */
        ARMv6(     CPUFamily.ARM,     true),
        /** ARM Cortex, usually little endian */
        ARMv7(     CPUFamily.ARM,     true),
        // 4

        /** X86 32bit, little endian */
        X86_32(    CPUFamily.X86,     true),
        /** PPC 32bit default, usually big endian */
        PPC(       CPUFamily.PPC,     true),
        /** MIPS 32bit, big endian (mips) or little endian (mipsel) */
        MIPS_32(   CPUFamily.MIPS,    true),
        /** Hitachi SuperH 32bit default, ??? endian */
        SuperH(    CPUFamily.SuperH,  true),
        /** SPARC 32bit, big endian */
        SPARC_32(  CPUFamily.SPARC,   true),
        // 9

        /** ARM64 default (64bit), usually little endian */
        ARM64(     CPUFamily.ARM,     false),
        /** ARM AArch64 (64bit), usually little endian */
        ARMv8_A(   CPUFamily.ARM,     false),
        /** X86 64bit, little endian */
        X86_64(    CPUFamily.X86,     false),
        /** PPC 64bit default, usually big endian */
        PPC64(     CPUFamily.PPC,     false),
        /** MIPS 64bit, big endian (mips64) or little endian (mipsel64) ? */
        MIPS_64(   CPUFamily.MIPS,    false),
        /** Itanium 64bit default, little endian */
        IA64(      CPUFamily.IA64,    false),
        /** SPARC 64bit, big endian */
        SPARCV9_64(CPUFamily.SPARC,   false),
        /** PA_RISC2_0 64bit, ??? endian */
        PA_RISC2_0(CPUFamily.PA_RISC, false);
        // 17

        public final CPUFamily family;
        public final boolean is32Bit;

        CPUType(final CPUFamily type, final boolean is32Bit){
            this.family = type;
            this.is32Bit = is32Bit;
        }

        /**
         * Returns {@code true} if the given {@link CPUType} is compatible
         * w/ this one, i.e. at least {@link #family} and {@link #is32Bit} is equal.
         */
        public final boolean isCompatible(final CPUType other) {
            if( null == other ) {
                return false;
            } else if( other == this ) {
                return true;
            } else {
                return this.family == other.family &&
                       this.is32Bit == other.is32Bit;
            }
        }

        public static final CPUType query(final String cpuABILower) {
            if( null == cpuABILower ) {
                throw new IllegalArgumentException("Null cpuABILower arg");
            }
            if(        cpuABILower.equals("x86")  ||
                       cpuABILower.equals("i386") ||
                       cpuABILower.equals("i486") ||
                       cpuABILower.equals("i586") ||
                       cpuABILower.equals("i686") ) {
                return X86_32;
            } else if( cpuABILower.equals("x86_64") ||
                       cpuABILower.equals("amd64")  ) {
                return X86_64;
            } else if( cpuABILower.equals("ia64") ) {
                return IA64;
            } else if( cpuABILower.equals("aarch64") ) {
                return ARM64;
            } else if( cpuABILower.startsWith("arm") ) {
                if(        cpuABILower.equals("armv8-a")   ||
                           cpuABILower.equals("arm-v8-a") ||
                           cpuABILower.equals("arm-8-a") ||
                           cpuABILower.equals("arm64-v8a") ) {
                    return ARMv8_A;
                } else if( cpuABILower.startsWith("arm64") ) {
                    return ARM64;
                } else if( cpuABILower.startsWith("armv7") ||
                           cpuABILower.startsWith("arm-v7") ||
                           cpuABILower.startsWith("arm-7") ||
                           cpuABILower.startsWith("armeabi-v7") ) {
                    return ARMv7;
                } else if( cpuABILower.startsWith("armv5") ||
                           cpuABILower.startsWith("arm-v5") ||
                           cpuABILower.startsWith("arm-5") ) {
                    return ARMv5;
                } else if( cpuABILower.startsWith("armv6") ||
                           cpuABILower.startsWith("arm-v6") ||
                           cpuABILower.startsWith("arm-6") ) {
                    return ARMv6;
                } else {
                    return ARM;
                }
            } else if( cpuABILower.equals("sparcv9") ) {
                return SPARCV9_64;
            } else if( cpuABILower.equals("sparc") ) {
                return SPARC_32;
            } else if( cpuABILower.equals("pa_risc2.0") ) {
                return PA_RISC2_0;
            } else if( cpuABILower.startsWith("ppc64") ) {
                return PPC64;
            } else if( cpuABILower.startsWith("ppc") ) {
                return PPC;
            } else if( cpuABILower.startsWith("mips64") ) {
                return MIPS_64;
            } else if( cpuABILower.startsWith("mips") ) {
                return MIPS_32;
            } else if( cpuABILower.startsWith("superh") ) {
                return SuperH;
            } else {
                throw new RuntimeException("Please port CPUType detection to your platform (CPU_ABI string '" + cpuABILower + "')");
            }
        }
    }

    public enum ABIType {
        GENERIC_ABI       ( 0x00 ),
        /** ARM GNU-EABI ARMEL -mfloat-abi=softfp */
        EABI_GNU_ARMEL    ( 0x01 ),
        /** ARM GNU-EABI ARMHF -mfloat-abi=hard */
        EABI_GNU_ARMHF    ( 0x02 ),
        /** ARM EABI AARCH64 (64bit) */
        EABI_AARCH64      ( 0x03 );

        public final int id;

        ABIType(final int id){
            this.id = id;
        }

        /**
         * Returns {@code true} if the given {@link ABIType} is compatible
         * w/ this one, i.e. they are equal.
         */
        public final boolean isCompatible(final ABIType other) {
            if( null == other ) {
                return false;
            } else {
                return other == this;
            }
        }

        public static final ABIType query(final CPUType cpuType, final String cpuABILower) {
            if( null == cpuType ) {
                throw new IllegalArgumentException("Null cpuType");
            } else if( null == cpuABILower ) {
                throw new IllegalArgumentException("Null cpuABILower");
            } else if( CPUFamily.ARM == cpuType.family ) {
                if( !cpuType.is32Bit ) {
                    return EABI_AARCH64;
                } else if( cpuABILower.equals("armeabi-v7a-hard") ) {
                    return EABI_GNU_ARMHF;
                } else {
                    return EABI_GNU_ARMEL;
                }
            } else {
                return GENERIC_ABI;
            }
        }
    }

    private static final String useTempJarCachePropName = "jogamp.gluegen.UseTempJarCache";

    /**
     * Fixed basename of JAR file and native library.
     * Dash replaced by underscore to allow static linkage via JEP 178.
     */
    private static final String libBaseName = "gluegen_rt";

    //
    // static initialization order:
    //

    /**
     * System property: 'jogamp.gluegen.UseTempJarCache',
     * defaults to true if {@link #OS_TYPE} is not {@link OSType#ANDROID}.
     */
    public static final boolean USE_TEMP_JAR_CACHE;

    //
    // post loading native lib:
    //

    private static final MachineDataInfo machineDescription;

    /** <code>true</code> if AWT is available and not in headless mode, otherwise <code>false</code>. */
    public static final boolean AWT_AVAILABLE;

    private static final boolean isRunningFromJarURL;

    static {
        final boolean[] _isRunningFromJarURL = new boolean[] { false };
        final boolean[] _USE_TEMP_JAR_CACHE = new boolean[] { false };
        final boolean[] _AWT_AVAILABLE = new boolean[] { false };

        SecurityUtil.doPrivileged(new PrivilegedAction<Object>() {
            @Override
            public Object run() {

                PlatformPropsImpl.initSingleton(); // documenting the order of static initialization

                final ClassLoader cl = Platform.class.getClassLoader();

                final Uri platformClassJarURI;
                {
                    Uri _platformClassJarURI = null;
                    try {
                        _platformClassJarURI = JarUtil.getJarUri(Platform.class.getName(), cl);
                    } catch (final Exception e) { }
                    platformClassJarURI = _platformClassJarURI;
                }
                _isRunningFromJarURL[0] = null != platformClassJarURI;

                _USE_TEMP_JAR_CACHE[0] = ( OS_TYPE != OSType.ANDROID ) && ( OS_TYPE != OSType.IOS ) &&
                                         ( null != platformClassJarURI ) &&
                                         PropertyAccess.getBooleanProperty(useTempJarCachePropName, true, true);

                // load GluegenRT native library
                if(_USE_TEMP_JAR_CACHE[0] && TempJarCache.initSingleton() && TempJarCache.isInitialized(true) ) {
                    try {
                        JNILibLoaderBase.addNativeJarLibs(new Class<?>[] { jogamp.common.Debug.class }, null);
                    } catch (final Exception e0) {
                        // IllegalArgumentException, IOException
                        System.err.println("Caught "+e0.getClass().getSimpleName()+": "+e0.getMessage()+", while JNILibLoaderBase.addNativeJarLibs(..)");
                    }
                }
                DynamicLibraryBundle.GlueJNILibLoader.loadLibrary(libBaseName, false, cl);

                // JVM bug workaround
                JVMUtil.initSingleton(); // requires gluegen_rt, one-time init.

                // AWT Headless determination
                if( !PropertyAccess.getBooleanProperty("java.awt.headless", true) &&
                    ReflectionUtil.isClassAvailable(ReflectionUtil.AWTNames.ComponentClass, cl) &&
                    ReflectionUtil.isClassAvailable(ReflectionUtil.AWTNames.GraphicsEnvironmentClass, cl) ) {
                    try {
                        _AWT_AVAILABLE[0] = false == ((Boolean)ReflectionUtil.callStaticMethod(ReflectionUtil.AWTNames.GraphicsEnvironmentClass, ReflectionUtil.AWTNames.isHeadlessMethod, null, null, cl)).booleanValue();
                    } catch (final Throwable t) { }
                }
                return null;
            } } );
        isRunningFromJarURL = _isRunningFromJarURL[0];
        USE_TEMP_JAR_CACHE = _USE_TEMP_JAR_CACHE[0];
        AWT_AVAILABLE = _AWT_AVAILABLE[0];

        //
        // Validate and setup MachineDataInfo.StaticConfig
        //
        MachineDataInfoRuntime.initialize();
        machineDescription = MachineDataInfoRuntime.getRuntime();
    }

    private Platform() {}

    /**
     * @return true if we're running from a Jar URL, otherwise false
     */
    public static final boolean isRunningFromJarURL() {
        return isRunningFromJarURL;
    }

    /**
     * kick off static initialization of <i>platform property information</i> and <i>native gluegen_rt lib loading</i>
     */
    public static void initSingleton() { }

    /**
     * Returns true if this machine is little endian, otherwise false.
     */
    public static boolean isLittleEndian() {
        return LITTLE_ENDIAN;
    }

    /**
     * Returns the OS name.
     * <p>In case of {@link OSType#ANDROID}, see {@link #getOSType()}, the OS name is Linux</p>
     */
    public static String getOSName() {
        return OS;
    }

    /**
     * Returns the OS version.
     */
    public static String getOSVersion() {
        return OS_VERSION;
    }

    /**
     * Returns the OS version number.
     */
    public static VersionNumber getOSVersionNumber() {
        return OS_VERSION_NUMBER;
    }

    /**
     * Returns the CPU architecture String.
     */
    public static String getArchName() {
        return ARCH;
    }

    /**
     * Returns the OS type.
     * <p>In case of {@link OSType#ANDROID} the {@link #getOSName() OS name}, is Linux</p>
     */
    public static OSType getOSType() {
        return OS_TYPE;
    }

    /**
     * Returns the CPU family.
     */
    public static CPUFamily getCPUFamily() {
        return CPU_ARCH.family;
    }

    /**
     * Returns the CPU architecture type.
     */
    public static CPUType getCPUType() {
        return CPU_ARCH;
    }

    /**
     * Returns true if this JVM/ARCH is 32bit.
     * <p>Shortcut to {@link #getCPUType()}.{@link CPUType#is32Bit is32Bit}</p>
     */
    public static boolean is32Bit() {
        return CPU_ARCH.is32Bit; // used very often
    }

    /**
     * Returns true if this JVM/ARCH is 64bit.
     * <p>Shortcut to !{@link #getCPUType()}.{@link CPUType#is32Bit is32Bit}</p>
     */
    public static boolean is64Bit() {
        return !CPU_ARCH.is32Bit; // used very often
    }

    /**
     * Returns the ABI type.
     * <p>
     * In case of {@link CPUFamily#ARM}, the value is determined by parsing the <i>Elf Headers</i> of the running VM.
     * </p>
     * <p>
     * Otherwise the value is {@link ABIType#GENERIC_ABI}.
     * </p>
     */
    public static ABIType getABIType() {
        return ABI_TYPE;
    }

    /**
     * Returns the GlueGen common name for the currently running OSType and CPUType
     * as implemented in the build system in 'gluegen-cpptasks-base.xml'.<br>
     *
     * @see #getOSAndArch(OSType, CPUType)
     */
    public static String getOSAndArch() {
        return os_and_arch;
    }

    /**
     * Returns the JAVA vendor.
     */
    public static String getJavaVendor() {
        return JAVA_VENDOR;
    }

    /**
     * Returns the JAVA VM name.
     */
    public static String getJavaVMName() {
        return JAVA_VM_NAME;
    }

    /**
     * Returns the JAVA runtime name.
     */
    public static String getJavaRuntimeName() {
        return JAVA_RUNTIME_NAME;
    }

    /**
     * Returns the JAVA vendor url.
     */
    public static String getJavaVendorURL() {
        return JAVA_VENDOR_URL;
    }

    /**
     * Returns the JAVA version.
     */
    public static String getJavaVersion() {
        return JAVA_VERSION;
    }

    /**
     * Returns the JAVA version number.
     */
    public static VersionNumber getJavaVersionNumber() {
        return JAVA_VERSION_NUMBER;
    }

    /**
     * Returns the platform's line separator.
     */
    public static String getNewline() {
        return NEWLINE;
    }

    /**
     * Returns the MachineDataInfo of the running machine.
     */
    public static MachineDataInfo getMachineDataInfo() {
        return machineDescription;
    }

    /** Returns <code>true</code> if AWT is available and not in headless mode, otherwise <code>false</code>. */
    public static boolean isAWTAvailable() {
        return AWT_AVAILABLE;
    }

    //
    // time / jitter
    //

    /**
     * Returns the unix based current time in milliseconds, see {@link Clock#currentTimeMillis()}.
     * @see Clock#currentTimeMillis()
     */
    public static long currentTimeMillis() {
        return Clock.currentTimeMillis();
    }

    /**
     * Returns the estimated sleep jitter value in nanoseconds.
     * <p>
     * Includes a warm-up path, allowing hotspot to optimize the code.
     * </p>
     */
    public static synchronized long getCurrentSleepJitter() {
        getCurrentSleepJitterImpl(TimeUnit.MILLISECONDS.toNanos(10), 10); // warm-up
        return getCurrentSleepJitterImpl(TimeUnit.MILLISECONDS.toNanos(10), 10);
    }
    private static long getCurrentSleepJitterImpl(final long nsDuration, final int splitInLoops) {
        final long nsPeriod = nsDuration / splitInLoops;
        final long t0_ns = Clock.currentNanos();
        for(int i=splitInLoops; i>0; i--) {
            try { TimeUnit.NANOSECONDS.sleep(nsPeriod); } catch (final InterruptedException e) { }
        }
        return  ( ( Clock.currentNanos() - t0_ns ) - nsDuration ) / splitInLoops;
    }

}

