/*
 * Copyright 2017-2018 Uber Technologies, Inc.
 *
 * 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 com.uber.h3core;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

/**
 * Extracts the native H3 core library to the local filesystem and loads it.
 */
public final class H3CoreLoader {
    private H3CoreLoader() {
        // Prevent instantiation
    }

    // Supported H3 architectures
    static final String ARCH_X64 = "x64";
    static final String ARCH_X86 = "x86";
    static final String ARCH_ARM64 = "arm64";

    private static volatile File libraryFile = null;

    /**
     * Read all bytes from <code>in</code> and write them to <code>out</code>.
     */
    private static void copyStream(InputStream in, OutputStream out) throws IOException {
        byte[] buf = new byte[4096];

        int read;
        while ((read = in.read(buf)) != -1) {
            out.write(buf, 0, read);
        }
    }

    /**
     * Copy the resource at the given path to the file. File will be made readable,
     * writable, and executable.
     *
     * @param resourcePath Resource to copy
     * @param newH3LibFile File to write
     * @throws UnsatisfiedLinkError The resource path does not exist
     */
    static void copyResource(String resourcePath, File newH3LibFile) throws IOException {
        // Set the permissions
        newH3LibFile.setReadable(true);
        newH3LibFile.setWritable(true, true);
        newH3LibFile.setExecutable(true, true);

        // Shove the resource into the file and close it
        try (InputStream resource = H3CoreLoader.class.getResourceAsStream(resourcePath)) {
            if (resource == null) {
                throw new UnsatisfiedLinkError(String.format("No native resource found at %s", resourcePath));
            }

            try (FileOutputStream outFile = new FileOutputStream(newH3LibFile)) {
                copyStream(resource, outFile);
            }
        }
    }

    /**
     * For use when the H3 library should be unpacked from the JAR and loaded.
     *
     * @throws SecurityException Loading the library was not allowed by the
     *                           SecurityManager.
     * @throws UnsatisfiedLinkError The library could not be loaded
     * @throws IOException Failed to unpack the library
     */
    public static NativeMethods loadNatives() throws IOException {
        final OperatingSystem os = detectOs(System.getProperty("java.vendor"), System.getProperty("os.name"));
        final String arch = detectArch(System.getProperty("os.arch"));

        return loadNatives(os, arch);
    }

    /**
     * For use when the H3 library should be unpacked from the JAR and loaded.
     * The native library for the specified operating system and architecture
     * will be extract.
     *
     * <p>H3 will only successfully extract the library once, even if different
     * operating system and architecture are specified, or if {@link #loadNatives()}
     * was used instead.
     *
     * @throws SecurityException Loading the library was not allowed by the
     *                           SecurityManager.
     * @throws UnsatisfiedLinkError The library could not be loaded
     * @throws IOException Failed to unpack the library
     * @param os Operating system whose lobrary should be used
     * @param arch Architecture name, as packaged in the H3 library
     */
    public synchronized static NativeMethods loadNatives(OperatingSystem os, String arch) throws IOException {
        // This is synchronized because if multiple threads were writing and
        // loading the shared object at the same time, bad things could happen.

        if (libraryFile == null) {
            final String dirName = String.format("%s-%s", os.getDirName(), arch);
            final String libName = String.format("libh3-java%s", os.getSuffix());

            final File newLibraryFile = File.createTempFile("libh3-java", os.getSuffix());

            newLibraryFile.deleteOnExit();

            copyResource(String.format("/%s/%s", dirName, libName), newLibraryFile);

            System.load(newLibraryFile.getCanonicalPath());

            libraryFile = newLibraryFile;
        }

        return new NativeMethods();
    }

    /**
     * For use when the H3 library is installed system-wide and Java is able to locate it.
     *
     * @throws SecurityException Loading the library was not allowed by the
     *                           SecurityManager.
     * @throws UnsatisfiedLinkError The library could not be loaded
     */
    public static NativeMethods loadSystemNatives() {
        System.loadLibrary("h3-java");

        return new NativeMethods();
    }

    /**
     * Operating systems supported by H3-Java.
     */
    public enum OperatingSystem {
        ANDROID(".so"),
        DARWIN(".dylib"),
        WINDOWS(".dll"),
        LINUX(".so");

        private final String suffix;

        OperatingSystem(String suffix) {
            this.suffix = suffix;
        }

        /**
         * Suffix for native libraries.
         */
        public String getSuffix() {
            return suffix;
        }

        /**
         * How this operating system's name is rendered when extracting the native library.
         */
        public String getDirName() {
            return name().toLowerCase();
        }
    }

    /**
     * Detect the current operating system.
     *
     * @param javaVendor Value of system property "java.vendor"
     * @param osName Value of system property "os.name"
     */
    static final OperatingSystem detectOs(String javaVendor, String osName) {
        // Detecting Android using the properties from:
        // https://developer.android.com/reference/java/lang/System.html
        if (javaVendor.toLowerCase().contains("android")) {
            return OperatingSystem.ANDROID;
        }

        String javaOs = osName.toLowerCase();
        if (javaOs.contains("mac")) {
            return OperatingSystem.DARWIN;
        } else if (javaOs.contains("win")) {
            return OperatingSystem.WINDOWS;
        } else {
            // Only other supported platform
            return OperatingSystem.LINUX;
        }
    }

    /**
     * Detect the system architecture.
     *
     * @param osArch Value of system property "os.arch"
     */
    static final String detectArch(String osArch) {
        if (osArch.equals("amd64") || osArch.equals("x86_64")) {
            return ARCH_X64;
        } else if (osArch.equals("i386") ||
                   osArch.equals("i486") ||
                   osArch.equals("i586") ||
                   osArch.equals("i686") ||
                   osArch.equals("i786") ||
                   osArch.equals("i886")) {
            return ARCH_X86;
        } else if (osArch.equals("aarch64")) {
            return ARCH_ARM64;
        } else {
            return osArch;
        }
    }
}