package jogamp.common.os;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import java.nio.ShortBuffer;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.List;

import jogamp.common.Debug;
import jogamp.common.os.elf.ElfHeaderPart1;
import jogamp.common.os.elf.ElfHeaderPart2;
import jogamp.common.os.elf.SectionArmAttributes;

import com.jogamp.common.nio.Buffers;
import com.jogamp.common.os.AndroidVersion;
import com.jogamp.common.os.NativeLibrary;
import com.jogamp.common.os.Platform;
import com.jogamp.common.os.Platform.ABIType;
import com.jogamp.common.os.Platform.CPUFamily;
import com.jogamp.common.os.Platform.CPUType;
import com.jogamp.common.os.Platform.OSType;
import com.jogamp.common.util.VersionNumber;

/**
 * Abstract parent class of {@link Platform} initializing and holding
 * platform information, which are initialized independent
 * of other classes.
 * <p>
 * This class is not intended to be exposed in the public namespace
 * and solely exist to solve initialization interdependencies.<br>
 * Please use {@link Platform} to access the public fields!
 * </p>
 */
public abstract class PlatformPropsImpl {
    static final boolean DEBUG = Debug.debug("Platform");

    /** Selected {@link Platform.OSType#MACOS} or {@link Platform.OSType#IOS} {@link VersionNumber}s. */
    public static class OSXVersion {
        /** OSX Tiger, i.e. 10.4.0 */
        public static final VersionNumber Tiger = new VersionNumber(10,4,0);
        /** OSX Lion, i.e. 10.7.0 */
        public static final VersionNumber Lion = new VersionNumber(10,7,0);
        /** OSX Mavericks, i.e. 10.9.0 */
        public static final VersionNumber Mavericks = new VersionNumber(10,9,0);
    }

    /**
     * Returns {@code true} if the given {@link CPUType}s and {@link ABIType}s are compatible.
     */
    public static final boolean isCompatible(final CPUType cpu1, final ABIType abi1, final CPUType cpu2, final ABIType abi2) {
        return cpu1.isCompatible(cpu2) && abi1.isCompatible(abi2);
    }

    //
    // static initialization order:
    //

    /** Version 1.6. As a JVM version, it enables certain JVM 1.6 features. */
    public static final VersionNumber Version16;
    /** Version 1.7. As a JVM version, it enables certain JVM 1.7 features. */
    public static final VersionNumber Version17;
    /** Version 1.8. As a JVM version, it enables certain JVM 1.8 features. */
    public static final VersionNumber Version18;
    /** Version 1.9. As a JVM version, it enables certain JVM 1.9 features. Note the skipped first version number due to JEP 223. */
    public static final VersionNumber Version9;

    public static final String OS;
    public static final String OS_lower;
    public static final String OS_VERSION;
    public static final VersionNumber OS_VERSION_NUMBER;
    public static final String ARCH;
    public static final String ARCH_lower;
    public static final String JAVA_VENDOR;
    public static final String JAVA_VENDOR_URL;
    public static final String JAVA_VERSION;
    public static final VersionNumber JAVA_VERSION_NUMBER;
    public static final int JAVA_VERSION_UPDATE;
    public static final String JAVA_VM_NAME;
    public static final String JAVA_RUNTIME_NAME;
    /** True if having {@link java.nio.LongBuffer} and {@link java.nio.DoubleBuffer} available. */
    public static final boolean JAVA_SE;
    /**
     * True only if being compatible w/ language level 6, e.g. JRE 1.6.
     * <p>
     * Implies {@link #isJavaSE()}.
     * </p>
     * <p>
     * <i>Note</i>: We claim Android is compatible.
     * </p>
     */
    public static final boolean JAVA_6;
    /**
     * True only if being compatible w/ language level 9, e.g. JRE 9.
     * <p>
     * Implies {@link #isJavaSE()} and {@link #JAVA_6}.
     * </p>
     * <p>
     * Since JRE 9, the version string has dropped the major release number,
     * see JEP 223: http://openjdk.java.net/jeps/223
     * </p>
     */
    public static final boolean JAVA_9;

    public static final String NEWLINE;
    public static final boolean LITTLE_ENDIAN;

    public static final CPUType CPU_ARCH;
    public static final ABIType ABI_TYPE;
    public static final OSType OS_TYPE;
    public static final String os_and_arch;
    /**
     * Usually GlueGen and subsequent JogAmp modules are build using dynamic libraries on supported platforms,
     * hence this boolean is expected to be true.
     * <p>
     * However, on certain systems static libraries are being used on which native JNI library loading is disabled.
     * </p>
     */
    public static final boolean useDynamicLibraries;

    static {
        Version16 = new VersionNumber(1, 6, 0);
        Version17 = new VersionNumber(1, 7, 0);
        Version18 = new VersionNumber(1, 8, 0);
        Version9 = new VersionNumber(9, 0, 0);

        // We don't seem to need an AccessController.doPrivileged() block
        // here as these system properties are visible even to unsigned Applets.
        final boolean isAndroid = AndroidVersion.isAvailable; // also triggers it's static initialization
        JAVA_VENDOR = System.getProperty("java.vendor");
        JAVA_VENDOR_URL = System.getProperty("java.vendor.url");
        JAVA_VERSION = System.getProperty("java.version");
        JAVA_VERSION_NUMBER = new VersionNumber(JAVA_VERSION);
        {
           int usIdx = JAVA_VERSION.lastIndexOf("-u"); // OpenJDK update notation
           int usOff;
           if( 0 < usIdx ) {
               usOff = 2;
           } else {
               usIdx = JAVA_VERSION.lastIndexOf("_"); // Oracle update notation
               usOff = 1;
           }
           if( 0 < usIdx ) {
               final String buildS = PlatformPropsImpl.JAVA_VERSION.substring(usIdx+usOff);
               final VersionNumber update = new VersionNumber(buildS);
               JAVA_VERSION_UPDATE = update.getMajor();
           } else {
               JAVA_VERSION_UPDATE = 0;
           }
        }
        JAVA_VM_NAME = System.getProperty("java.vm.name");
        JAVA_RUNTIME_NAME = getJavaRuntimeNameImpl();
        JAVA_SE = initIsJavaSE();
        JAVA_9 = JAVA_SE && JAVA_VERSION_NUMBER.compareTo(Version9) >= 0;
        JAVA_6 = JAVA_SE && ( isAndroid || JAVA_9 || JAVA_VERSION_NUMBER.compareTo(Version16) >= 0 ) ;

        NEWLINE = System.getProperty("line.separator");

        OS =  System.getProperty("os.name");
        OS_lower = OS.toLowerCase();
        OS_VERSION =  System.getProperty("os.version");
        OS_VERSION_NUMBER = new VersionNumber(OS_VERSION);
        OS_TYPE = getOSTypeImpl(OS_lower, isAndroid);

        // Hard values, i.e. w/ probing binaries
        final String elfCpuName;
        final CPUType elfCpuType;
        final ABIType elfABIType;
        final int elfLittleEndian;
        final boolean elfValid;
        {
            final String[] _elfCpuName = { null };
            final CPUType[] _elfCpuType = { null };
            final ABIType[] _elfAbiType = { null };
            final int[] _elfLittleEndian = { 0 }; // 1 - little, 2 - big
            final boolean[] _elfValid = { false };
            AccessController.doPrivileged(new PrivilegedAction<Object>() {
                @Override
                public Object run() {
                    RandomAccessFile in = null;
                    try {
                        final File file = queryElfFile(OS_TYPE);
                        if(DEBUG) {
                            System.err.println("ELF-1: Using "+file);
                        }
                        in = new RandomAccessFile(file, "r");
                        final ElfHeaderPart1 eh1 = readElfHeaderPart1(OS_TYPE, in);
                        if(DEBUG) {
                            System.err.println("ELF-1: Got "+eh1);
                        }
                        if( null != eh1 ) {
                            final ElfHeaderPart2 eh2 = readElfHeaderPart2(eh1, in);
                            if(DEBUG) {
                                System.err.println("ELF-2: Got "+eh2);
                            }
                            if( null != eh2 ) {
                                _elfCpuName[0] = eh2.cpuName;
                                _elfCpuType[0] = eh2.cpuType;
                                _elfAbiType[0] = eh2.abiType;
                                if( eh1.isLittleEndian() ) {
                                    _elfLittleEndian[0] = 1;
                                } else if( eh1.isBigEndian() ) {
                                    _elfLittleEndian[0] = 2;
                                }
                                _elfValid[0] = true;
                            }
                        }
                    } catch (final Throwable t) {
                        if(DEBUG) {
                            t.printStackTrace();
                        }
                    } finally {
                        if(null != in) {
                            try {
                                in.close();
                            } catch (final IOException e) { }
                        }
                    }
                    return null;
                } });
            elfCpuName = _elfCpuName[0];
            elfCpuType = _elfCpuType[0];
            elfABIType = _elfAbiType[0];
            elfLittleEndian = _elfLittleEndian[0];
            elfValid = _elfValid[0];
            if( DEBUG ) {
                System.err.println("Platform.Elf: valid "+elfValid+", elfCpuName "+elfCpuName+", cpuType "+elfCpuType+", abiType "+elfABIType+", elfLittleEndian "+elfLittleEndian);
            }
        }

        // Determine endianess, favor ELF value
        final boolean littleEndian = queryIsLittleEndianImpl();
        if( elfValid ) {
            switch( elfLittleEndian ) {
                case 1:
                    LITTLE_ENDIAN = true;
                    break;
                case 2:
                    LITTLE_ENDIAN = false;
                    break;
                default:
                    LITTLE_ENDIAN = littleEndian;
                    break;
            }
        } else {
            LITTLE_ENDIAN = littleEndian;
        }
        if( DEBUG ) {
            System.err.println("Platform.Endian: test-little "+littleEndian+", elf[valid "+elfValid+", val "+elfLittleEndian+"] -> LITTLE_ENDIAN "+LITTLE_ENDIAN);
        }

        // Property values for comparison
        // We might take the property values even if ELF values are available,
        // since the latter only reflect the CPU/ABI version of the binary files!
        final String propARCH = System.getProperty("os.arch");
        final String propARCH_lower = propARCH.toLowerCase();
        final CPUType propCpuType = CPUType.query(propARCH_lower);
        final ABIType propABIType = ABIType.query(propCpuType, propARCH_lower);
        if( DEBUG ) {
            System.err.println("Platform.Property: ARCH "+propARCH+", CpuType "+propCpuType+", ABIType "+propABIType);
        }

        final int strategy;
        if( isAndroid ) {
            if( DEBUG ) {
                System.err.println("Android: CPU_ABI1 str "+AndroidVersion.CPU_ABI+", CPU_TYPE "+AndroidVersion.CPU_TYPE+", ABI_TYPE "+AndroidVersion.ABI_TYPE);
                System.err.println("Android: CPU_ABI2 str "+AndroidVersion.CPU_ABI2+", CPU_TYPE2 "+AndroidVersion.CPU_TYPE2+", ABI_TYPE2 "+AndroidVersion.ABI_TYPE2);
            }
            if( elfValid ) {
                if( null != AndroidVersion.CPU_TYPE &&
                    isCompatible(elfCpuType, elfABIType, AndroidVersion.CPU_TYPE, AndroidVersion.ABI_TYPE) )
                {
                    // ELF matches Android-1
                    ARCH = AndroidVersion.CPU_ABI;
                    ARCH_lower = ARCH;
                    CPU_ARCH = AndroidVersion.CPU_TYPE;
                    strategy = 110;
                } else if( null != AndroidVersion.CPU_TYPE2 &&
                           isCompatible(elfCpuType, elfABIType, AndroidVersion.CPU_TYPE2, AndroidVersion.ABI_TYPE2) )
                {
                    // ELF matches Android-2
                    ARCH = AndroidVersion.CPU_ABI2;
                    ARCH_lower = ARCH;
                    CPU_ARCH = AndroidVersion.CPU_TYPE2;
                    strategy = 111;
                } else {
                    // We assume our ELF data beats AndroidVersion info (correctness)
                    ARCH = elfCpuType.toString();
                    ARCH_lower = ARCH.toLowerCase();
                    CPU_ARCH = elfCpuType;
                    strategy = 112;
                }
                ABI_TYPE = elfABIType;
            } else {
                if( AndroidVersion.CPU_TYPE.family == CPUFamily.ARM || AndroidVersion.CPU_TYPE.family == CPUFamily.X86 ||
                    null == AndroidVersion.CPU_TYPE2 ) {
                    // Favor Android-1: Either b/c ARM or x86 Family, or no Android-2
                    ARCH = AndroidVersion.CPU_ABI;
                    ARCH_lower = ARCH;
                    CPU_ARCH = AndroidVersion.CPU_TYPE;
                    ABI_TYPE = AndroidVersion.ABI_TYPE;
                    strategy = 120;
                } else {
                    // Last resort Android-2
                    ARCH = AndroidVersion.CPU_ABI2;
                    ARCH_lower = ARCH;
                    CPU_ARCH = AndroidVersion.CPU_TYPE2;
                    ABI_TYPE = AndroidVersion.ABI_TYPE2;
                    strategy = 121;
                }
            }
        } else {
            if( elfValid ) {
                if( isCompatible(elfCpuType, elfABIType, propCpuType, propABIType) ) {
                    // Use property ARCH, compatible w/ ELF
                    ARCH = propARCH;
                    ARCH_lower = propARCH_lower;
                    CPU_ARCH = propCpuType;
                    ABI_TYPE = propABIType;
                    strategy = 210;
                } else {
                    // use ELF ARCH
                    ARCH = elfCpuName;
                    ARCH_lower = elfCpuName;
                    CPU_ARCH = elfCpuType;
                    ABI_TYPE = elfABIType;
                    strategy = 211;
                }
            } else {
                // Last resort: properties
                ARCH = propARCH;
                ARCH_lower = propARCH_lower;
                CPU_ARCH = propCpuType;
                ABI_TYPE = propABIType;
                strategy = 220;
            }
        }
        if( OSType.IOS == OS_TYPE ) {
            useDynamicLibraries = false;
        } else {
            useDynamicLibraries = true;
        }
        if( DEBUG ) {
            System.err.println("Platform.Hard: ARCH "+ARCH+", CPU_ARCH "+CPU_ARCH+", ABI_TYPE "+ABI_TYPE+" - strategy "+strategy+"(isAndroid "+isAndroid+", elfValid "+elfValid+"), useDynLibs "+useDynamicLibraries);
        }
        os_and_arch = getOSAndArch(OS_TYPE, CPU_ARCH, ABI_TYPE, LITTLE_ENDIAN);
    }

    protected PlatformPropsImpl() {}

    private static final String getJavaRuntimeNameImpl() {
        // the fast path, check property Java SE instead of traversing through the ClassLoader
        return AccessController.doPrivileged(new PrivilegedAction<String>() {
            @Override
            public String run() {
              return System.getProperty("java.runtime.name");
            }
          });
    }

    private static final boolean initIsJavaSE() {
        if( null != JAVA_RUNTIME_NAME && JAVA_RUNTIME_NAME.indexOf("Java SE") != -1) {
            return true;
        }

        // probe for classes we need on a SE environment
        try {
            Class.forName("java.nio.LongBuffer");
            Class.forName("java.nio.DoubleBuffer");
            return true;
        } catch(final ClassNotFoundException ex) {
            // continue with Java SE check
        }

        return false;
    }

    private static final boolean queryIsLittleEndianImpl() {
        final ByteBuffer tst_b = Buffers.newDirectByteBuffer(Buffers.SIZEOF_INT); // 32bit in native order
        final IntBuffer tst_i = tst_b.asIntBuffer();
        final ShortBuffer tst_s = tst_b.asShortBuffer();
        tst_i.put(0, 0x0A0B0C0D);
        return 0x0C0D == tst_s.get(0);
    }

    @SuppressWarnings("unused")
    private static final boolean contains(final String data, final String[] search) {
        if(null != data && null != search) {
            for(int i=0; i<search.length; i++) {
                if(data.indexOf(search[i]) >= 0) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Returns the {@link ABIType} of the current platform using given {@link CPUType cpuType}
     * and {@link OSType osType} as a hint.
     * <p>
     * For Elf parsing one of the following binaries is used:
     * <ul>
     *  <li>Linux: Current executable</li>
     *  <li>Android: Found gluegen_rt library</li>
     *  <li>Other: A found java/jvm native library.</li>
     * </ul>
     * </p>
     * <p>
     * Elf ARM Tags are read using {@link ElfHeader}, .. and {@link SectionArmAttributes#abiVFPArgsAcceptsVFPVariant(byte)}.
     * </p>
     */
    private static final File queryElfFile(final OSType osType) {
        File file = null;
        try {
            if( OSType.ANDROID == osType ) {
                file = new File(NativeLibrary.findLibrary("gluegen_rt", PlatformPropsImpl.class.getClassLoader()));
            } else {
                if( OSType.LINUX == osType ) {
                    file = new File("/proc/self/exe");
                    if( !checkFileReadAccess(file) ) {
                        file = null;
                    }
                }
                if( null == file ) {
                    file = findSysLib("java");
                }
                if( null == file ) {
                    file = findSysLib("jvm");
                }
            }
        } catch(final Throwable t) {
            if(DEBUG) {
                t.printStackTrace();
            }
        }
        return file;
    }
    private static final ElfHeaderPart1 readElfHeaderPart1(final OSType osType, final RandomAccessFile in) {
        ElfHeaderPart1 res = null;
        try {
            res = ElfHeaderPart1.read(osType, in);
        } catch(final Throwable t) {
            if(DEBUG) {
                System.err.println("Caught: "+t.getMessage());
                t.printStackTrace();
            }
        }
        return res;
    }
    private static final ElfHeaderPart2 readElfHeaderPart2(final ElfHeaderPart1 eh1, final RandomAccessFile in) {
        ElfHeaderPart2 res = null;
        try {
            res = ElfHeaderPart2.read(eh1, in);
        } catch(final Throwable t) {
            if(DEBUG) {
                System.err.println("Caught: "+t.getMessage());
                t.printStackTrace();
            }
        }
        return res;
    }
    private static boolean checkFileReadAccess(final File file) {
        try {
            return file.isFile() && file.canRead();
        } catch (final Throwable t) { }
        return false;
    }
    private static File findSysLib(final String libName) {
        final ClassLoader cl = PlatformPropsImpl.class.getClassLoader();
        final List<String> possibleLibPaths = NativeLibrary.enumerateLibraryPaths(libName, libName, libName, true, cl);
        for(int i=0; i<possibleLibPaths.size(); i++) {
            final String libPath = possibleLibPaths.get(i);
            final File lib = new File(libPath);
            if(DEBUG) {
                System.err.println("findSysLib #"+i+": test "+lib);
            }
            if( checkFileReadAccess(lib) ) {
                return lib;
            }
            if(DEBUG) {
                System.err.println("findSysLib #"+i+": "+lib+" not readable");
            }
        }
        return null;
    }

    private static final OSType getOSTypeImpl(final String osLower, final boolean isAndroid) throws RuntimeException {
        if ( isAndroid ) {
            return OSType.ANDROID;
        }
        if ( osLower.startsWith("linux") ) {
            return OSType.LINUX;
        }
        if ( osLower.startsWith("freebsd") ) {
            return OSType.FREEBSD;
        }
        if ( osLower.startsWith("android") ) {
            return OSType.ANDROID;
        }
        if ( osLower.startsWith("mac os x") ||
             osLower.startsWith("darwin") ) {
            return OSType.MACOS;
        }
        if ( osLower.startsWith("sunos") ) {
            return OSType.SUNOS;
        }
        if ( osLower.startsWith("hp-ux") ) {
            return OSType.HPUX;
        }
        if ( osLower.startsWith("windows") ) {
            return OSType.WINDOWS;
        }
        if ( osLower.startsWith("kd") ) {
            return OSType.OPENKODE;
        }
        if ( osLower.startsWith("ios") ) {
            return OSType.IOS;
        }
        throw new RuntimeException("Please port OS detection to your platform (" + OS_lower + "/" + ARCH_lower + ")");
    }

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

    /**
     * Returns the GlueGen common name for the given
     * {@link OSType}, {@link CPUType}, {@link ABIType} and {@code littleEndian}.
     * <p>
     * Consult 'gluegen/make/gluegen-cpptasks-base.xml' to complete/sync mapping!
     * </p>
     *
     * An excerpt of supported <code>os.and.arch</code> strings:
     * <ul>
     *   <li>android-armv6</li>
     *   <li>android-aarch64</li>
     *   <li>android-x86</li>
     *   <li>linux-armv6</li>
     *   <li>linux-armv6hf</li>
     *   <li>linux-i586</li>
     *   <li>linux-ppc</li>
     *   <li>linux-mips</li>
     *   <li>linux-mipsel</li>
     *   <li>linux-superh</li>
     *   <li>linux-sparc</li>
     *   <li>linux-aarch64</li>
     *   <li>linux-amd64</li>
     *   <li>linux-ppc64</li>
     *   <li>linux-ppc64le</li>
     *   <li>linux-mips64</li>
     *   <li>linux-ia64</li>
     *   <li>linux-sparcv9</li>
     *   <li>linux-risc2.0</li>
     *   <li>freebsd-i586</li>
     *   <li>freebsd-amd64</li>
     *   <li>hpux-hppa</li>
     *   <li>macosx-universal</li>
     *   <li>solaris-sparc</li>
     *   <li>solaris-sparcv9</li>
     *   <li>solaris-amd64</li>
     *   <li>solaris-i586</li>
     *   <li>windows-amd64</li>
     *   <li>windows-i586</li>
     * </ul>
     * @return The <i>os.and.arch</i> value.
     */
    public static final String getOSAndArch(final OSType osType, final CPUType cpuType, final ABIType abiType, final boolean littleEndian) {
        final String os_;
        final String _and_arch_tmp, _and_arch_final;

        switch( cpuType ) {
            case ARM:
            case ARMv5:
            case ARMv6:
            case ARMv7:
                if( ABIType.EABI_GNU_ARMHF == abiType ) {
                    _and_arch_tmp = "armv6hf";
                } else {
                    _and_arch_tmp = "armv6";
                }
                break;
            case X86_32:
                _and_arch_tmp = "i586";
                break;
            case PPC:
                _and_arch_tmp = "ppc";
                break;
            case MIPS_32:
                _and_arch_tmp = littleEndian ? "mipsel" : "mips";
                break;
            case SuperH:
                _and_arch_tmp = "superh";
                break;
            case SPARC_32:
                _and_arch_tmp = "sparc";
                break;

            case ARM64:
            case ARMv8_A:
                _and_arch_tmp = "aarch64";
                break;
            case X86_64:
                _and_arch_tmp = "amd64";
                break;
            case PPC64:
                _and_arch_tmp = littleEndian ? "ppc64le" : "ppc64";
                break;
            case MIPS_64:
                _and_arch_tmp = "mips64";
                break;
            case IA64:
                _and_arch_tmp = "ia64";
                break;
            case SPARCV9_64:
                _and_arch_tmp = "sparcv9";
                break;
            case PA_RISC2_0:
                _and_arch_tmp = "risc2.0";
                break;
            default:
                throw new InternalError("Unhandled CPUType: "+cpuType);
        }

        switch( osType ) {
            case ANDROID:
              os_ = "android";
              _and_arch_final = _and_arch_tmp;
              break;
            case MACOS:
              os_ = "macosx";
              _and_arch_final = "universal";
              break;
            case IOS:
              os_ = "ios";
              _and_arch_final = _and_arch_tmp;
              break;
            case WINDOWS:
              os_ = "windows";
              _and_arch_final = _and_arch_tmp;
              break;
            case OPENKODE:
              os_ = "openkode";             // TODO: think about that
              _and_arch_final = _and_arch_tmp;
              break;
            case LINUX:
              os_ = "linux";
              _and_arch_final = _and_arch_tmp;
              break;
            case FREEBSD:
              os_ = "freebsd";
              _and_arch_final = _and_arch_tmp;
              break;
            case SUNOS:
              os_ = "solaris";
              _and_arch_final = _and_arch_tmp;
              break;
            case HPUX:
              os_ = "hpux";
              _and_arch_final = "hppa";     // TODO: really only hppa ?
              break;
            default:
              throw new InternalError("Unhandled OSType: "+osType);
        }
        return os_ + "-" + _and_arch_final;
    }

}