/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2014 Red Hat, Inc., and individual contributors
 * as indicated by the @author tags.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.jboss.modules;

import java.io.File;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Locale;

/**
 * A base class for resource loaders which can load native libraries.
 *
 * @author <a href="mailto:david.lloyd@redhat.com">David M. Lloyd</a>
 */
public class NativeLibraryResourceLoader extends AbstractResourceLoader {

    /**
     * Separate class for native platform ID which is only loaded when native libs are loaded.
     */
    static class Identification {
        static final String OS_ID;
        static final String CPU_ID;
        static final String ARCH_NAME;
        static final String[] NATIVE_SEARCH_PATHS;

        static {
            final Object[] strings = AccessController.doPrivileged(new PrivilegedAction<Object[]>() {
                public Object[] run() {
                    // First, identify the operating system.
                    boolean knownOs = true;
                    String osName;
                    // let the user override it.
                    osName = System.getProperty("jboss.modules.os-name");
                    if (osName == null) {
                        String sysOs = System.getProperty("os.name");
                        if (sysOs == null) {
                            osName = "unknown";
                            knownOs = false;
                        } else {
                            sysOs = sysOs.toUpperCase(Locale.US);
                            if (sysOs.startsWith("LINUX")) {
                                osName = "linux";
                            } else if (sysOs.startsWith("MAC OS")) {
                                osName = "macosx";
                            } else if (sysOs.startsWith("WINDOWS")) {
                                osName = "win";
                            } else if (sysOs.startsWith("OS/2")) {
                                osName = "os2";
                            } else if (sysOs.startsWith("SOLARIS") || sysOs.startsWith("SUNOS")) {
                                osName = "solaris";
                            } else if (sysOs.startsWith("MPE/IX")) {
                                osName = "mpeix";
                            } else if (sysOs.startsWith("HP-UX")) {
                                osName = "hpux";
                            } else if (sysOs.startsWith("AIX")) {
                                osName = "aix";
                            } else if (sysOs.startsWith("OS/390")) {
                                osName = "os390";
                            } else if (sysOs.startsWith("OS/400")) {
                                osName = "os400";
                            } else if (sysOs.startsWith("FREEBSD")) {
                                osName = "freebsd";
                            } else if (sysOs.startsWith("OPENBSD")) {
                                osName = "openbsd";
                            } else if (sysOs.startsWith("NETBSD")) {
                                osName = "netbsd";
                            } else if (sysOs.startsWith("IRIX")) {
                                osName = "irix";
                            } else if (sysOs.startsWith("DIGITAL UNIX")) {
                                osName = "digitalunix";
                            } else if (sysOs.startsWith("OSF1")) {
                                osName = "osf1";
                            } else if (sysOs.startsWith("OPENVMS")) {
                                osName = "openvms";
                            } else if (sysOs.startsWith("IOS")) {
                                osName = "iOS";
                            } else {
                                osName = "unknown";
                                knownOs = false;
                            }
                        }
                    }
                    // Next, our CPU ID and its compatible variants.
                    boolean knownCpu = true;
                    ArrayList<String> cpuNames = new ArrayList<>();

                    String cpuName = System.getProperty("jboss.modules.cpu-name");
                    if (cpuName == null) {
                        String sysArch = System.getProperty("os.arch");
                        if (sysArch == null) {
                            cpuName = "unknown";
                            knownCpu = false;
                        } else {
                            boolean hasEndian = false;
                            boolean hasHardFloatABI = false;
                            sysArch = sysArch.toUpperCase(Locale.US);
                            if (sysArch.startsWith("SPARCV9") || sysArch.startsWith("SPARC64")) {
                                cpuName = "sparcv9";
                            } else if (sysArch.startsWith("SPARC")) {
                                cpuName = "sparc";
                            } else if (sysArch.startsWith("X86_64") || sysArch.startsWith("AMD64")) {
                                cpuName = "x86_64";
                            } else if (sysArch.startsWith("I386")) {
                                cpuName = "i386";
                            } else if (sysArch.startsWith("I486")) {
                                cpuName = "i486";
                            } else if (sysArch.startsWith("I586")) {
                                cpuName = "i586";
                            } else if (sysArch.startsWith("I686") || sysArch.startsWith("X86") || sysArch.contains("IA32")) {
                                cpuName = "i686";
                            } else if (sysArch.startsWith("X32")) {
                                cpuName = "x32";
                            } else if (sysArch.startsWith("PPC64")) {
                                cpuName = "ppc64";
                            } else if (sysArch.startsWith("PPC") || sysArch.startsWith("POWER")) {
                                cpuName = "ppc";
                            } else if (sysArch.startsWith("ARMV7A") || sysArch.contains("AARCH32")) {
                                hasEndian = true;
                                hasHardFloatABI = true;
                                cpuName = "armv7a";
                            } else if (sysArch.startsWith("AARCH64") || sysArch.startsWith("ARM64") || sysArch.startsWith("ARMV8") || sysArch.startsWith("PXA9") || sysArch.startsWith("PXA10")) {
                                hasEndian = true;
                                cpuName = "aarch64";
                            } else if (sysArch.startsWith("PXA27")) {
                                hasEndian = true;
                                cpuName = "armv5t-iwmmx";
                            } else if (sysArch.startsWith("PXA3")) {
                                hasEndian = true;
                                cpuName = "armv5t-iwmmx2";
                            } else if (sysArch.startsWith("ARMV4T") || sysArch.startsWith("EP93")) {
                                hasEndian = true;
                                cpuName = "armv4t";
                            } else if (sysArch.startsWith("ARMV4") || sysArch.startsWith("EP73")) {
                                hasEndian = true;
                                cpuName = "armv4";
                            } else if (sysArch.startsWith("ARMV5T") || sysArch.startsWith("PXA") || sysArch.startsWith("IXC") || sysArch.startsWith("IOP") || sysArch.startsWith("IXP") || sysArch.startsWith("CE")) {
                                hasEndian = true;
                                String isaList = System.getProperty("sun.arch.isalist");
                                if (isaList != null) {
                                    if (isaList.toUpperCase(Locale.US).contains("MMX2")) {
                                        cpuName = "armv5t-iwmmx2";
                                    } else if (isaList.toUpperCase(Locale.US).contains("MMX")) {
                                        cpuName = "armv5t-iwmmx";
                                    } else {
                                        cpuName = "armv5t";
                                    }
                                } else {
                                    cpuName = "armv5t";
                                }
                            } else if (sysArch.startsWith("ARMV5")) {
                                hasEndian = true;
                                cpuName = "armv5";
                            } else if (sysArch.startsWith("ARMV6")) {
                                hasEndian = true;
                                hasHardFloatABI = true;
                                cpuName = "armv6";
                            } else if (sysArch.startsWith("PA_RISC2.0W")) {
                                cpuName = "parisc64";
                            } else if (sysArch.startsWith("PA_RISC") || sysArch.startsWith("PA-RISC")) {
                                cpuName = "parisc";
                            } else if (sysArch.startsWith("IA64")) {
                                // HP-UX reports IA64W for 64-bit Itanium and IA64N when running
                                // in 32-bit mode.
                                cpuName = sysArch.toLowerCase(Locale.US);
                            } else if (sysArch.startsWith("ALPHA")) {
                                cpuName = "alpha";
                            } else if (sysArch.startsWith("MIPS")) {
                                cpuName = "mips";
                            } else {
                                knownCpu = false;
                                cpuName = "unknown";
                            }

                            boolean be = false;
                            boolean hf = false;

                            if (knownCpu && hasEndian && "big".equals(System.getProperty("sun.cpu.endian", "little"))) {
                                be = true;
                            }

                            if (knownCpu && hasHardFloatABI) {
                                String archAbi = System.getProperty("sun.arch.abi");
                                if (archAbi != null) {
                                    if (archAbi.toUpperCase(Locale.US).contains("HF")) {
                                        hf = true;
                                    }
                                } else {
                                    String libPath = System.getProperty("java.library.path");
                                    if (libPath != null && libPath.toUpperCase(Locale.US).contains("GNUEABIHF")) {
                                        hf = true;
                                    }
                                }
                                if (hf) cpuName += "-hf";
                            }

                            if (knownCpu) {
                                switch (cpuName) {
                                    case "i686": cpuNames.add("i686");
                                    case "i586": cpuNames.add("i586");
                                    case "i486": cpuNames.add("i486");
                                    case "i386": cpuNames.add("i386");
                                        break;
                                    case "armv7a": cpuNames.add("armv7a"); if (hf) break;
                                    case "armv6":  cpuNames.add("armv6"); if (hf) break;
                                    case "armv5t": cpuNames.add("armv5t");
                                    case "armv5":  cpuNames.add("armv5");
                                    case "armv4t": cpuNames.add("armv4t");
                                    case "armv4":  cpuNames.add("armv4");
                                        break;
                                    case "armv5t-iwmmx2": cpuNames.add("armv5t-iwmmx2");
                                    case "armv5t-iwmmx":  cpuNames.add("armv5t-iwmmx");
                                                          cpuNames.add("armv5t");
                                                          cpuNames.add("armv5");
                                                          cpuNames.add("armv4t");
                                                          cpuNames.add("armv4");
                                        break;
                                    default: cpuNames.add(cpuName);
                                        break;
                                }
                                if (hf || be) for (int i = 0; i < cpuNames.size(); i++) {
                                    String name = cpuNames.get(i);
                                    if (be) name += "-be";
                                    if (hf) name += "-hf";
                                    cpuNames.set(i, name);
                                }
                                cpuName = cpuNames.get(0);
                            }
                        }
                    }

                    // Finally, search paths.
                    final int cpuCount = cpuNames.size();
                    String[] searchPaths = new String[cpuCount];
                    if (knownOs && knownCpu) {
                        for (int i = 0; i < cpuCount; i++) {
                            final String name = cpuNames.get(i);
                            searchPaths[i] = osName + "-" + name;
                        }
                    } else {
                        searchPaths = new String[0];
                    }

                    return new Object[] {
                        osName,
                        cpuName,
                        osName + "-" + cpuName,
                        searchPaths
                    };
                }
            });
            OS_ID = strings[0].toString();
            CPU_ID = strings[1].toString();
            ARCH_NAME = strings[2].toString();
            NATIVE_SEARCH_PATHS = (String[]) strings[3];
        }
    }

    /**
     * The filesystem root of the resource loader.
     */
    private final File root;

    /**
     * Construct a new instance.
     *
     * @param root the filesystem root of the resource loader
     */
    public NativeLibraryResourceLoader(final File root) {
        this.root = root;
    }

    /** {@inheritDoc} */
    public String getLibrary(final String name) {
        final String mappedName = System.mapLibraryName(name);
        final File root = this.root;
        File testFile;
        for (String path : Identification.NATIVE_SEARCH_PATHS) {
            testFile = new File(new File(root, path), mappedName);
            if (testFile.exists()) {
                return testFile.getAbsolutePath();
            }
        }
        return null;
    }

    /**
     * Get the filesystem root of the resource loader.
     *
     * @return the filesystem root of the resource loader
     */
    public File getRoot() {
        return root;
    }

    /**
     * Get the detected architecture name for this platform.
     *
     * @return the architecture name
     */
    public static String getArchName() {
        return Identification.ARCH_NAME;
    }
}
