/*
 * Copyright (c) 2008, Harald Kuhr
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * 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.
 *     * Neither the name "TwelveMonkeys" nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "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 THE COPYRIGHT OWNER 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.
 */

package com.twelvemonkeys.lang;

import com.twelvemonkeys.io.FileUtil;
import com.twelvemonkeys.util.FilterIterator;
import com.twelvemonkeys.util.service.ServiceRegistry;

import java.io.*;
import java.util.Collections;
import java.util.Iterator;

/**
 * NativeLoader
 * <p/>
 *
 * @author <a href="mailto:harald.kuhr@gmail.com">Harald Kuhr</a>
 * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/NativeLoader.java#2 $
 */
final class NativeLoader {
    // TODO: Considerations:
    // - Rename all libs like the current code, to <library>.(so|dll|dylib)?
    // - Keep library filename from jar, and rather store a separate
    //   properties-file with the library->library-file mappings?
    // - As all invocations are with library file name, we could probably skip
    //   both renaming and properties-file altogether...

    // TODO: The real trick here, is how to load the correct library for the
    // current platform...
    // - Change String pResource to String[] pResources?
    // - NativeResource class, that has a list of multiple resources?
    //   NativeResource(Map<String, String>) os->native lib mapping

    // TODO: Consider exposing the method from SystemUtil

    // TODO: How about a SPI based solution?!
    // public interface com.twelvemonkeys.lang.NativeResourceProvider
    //
    // imlementations return a pointer to the correct resource for a given (by
    // this class) OS.
    //
    // String getResourceName(...)
    //
    // See http://tolstoy.com/samizdat/sysprops.html
    // System properties:
    // "os.name"
    //      Windows, Linux, Solaris/SunOS,
    //      Mac OS/Mac OS X/Rhapsody (aka Mac OS X Server)
    //      General Unix (AIX, Digital Unix, FreeBSD, HP-UX, Irix)
    //      OS/2
    // "os.arch"
    //      Windows: x86
    //      Linux: x86, i386, i686, x86_64, ia64,
    //      Solaris: sparc, sparcv9, x86
    //      Mac OS: PowerPC, ppc, i386
    //      AIX: x86, ppc
    //      Digital Unix: alpha
    //      FreeBSD: x86, sparc
    //      HP-UX: PA-RISC
    //      Irix: mips
    //      OS/2: x86
    // "os.version"
    //      Windows: 4.0 -> NT/95, 5.0 -> 2000, 5.1 -> XP (don't care about old versions, CE etc)
    //      Mac OS: 8.0, 8.1, 10.0 -> OS X, 10.x.x -> OS X, 5.6 -> Rhapsody (!)
    //
    // Normalize os.name, os.arch and os.version?!


    ///** Normalized operating system constant */
    //static final OperatingSystem OS_NAME = normalizeOperatingSystem();
    //
    ///** Normalized system architecture constant */
    //static final Architecture OS_ARCHITECTURE = normalizeArchitecture();
    //
    ///** Unormalized operating system version constant (for completeness) */
    //static final String OS_VERSION = System.getProperty("os.version");

    static final NativeResourceRegistry sRegistry = new NativeResourceRegistry();

    private NativeLoader() {
    }

/*
    private static Architecture normalizeArchitecture() {
        String arch = System.getProperty("os.arch");
        if (arch == null) {
            throw new IllegalStateException("System property \"os.arch\" == null");
        }

        arch = arch.toLowerCase();
        if (OS_NAME == OperatingSystem.Windows
                && (arch.startsWith("x86") || arch.startsWith("i386"))) {
            return Architecture.X86;
            // TODO: 64 bit
        }
        else if (OS_NAME == OperatingSystem.Linux) {
            if  (arch.startsWith("x86") || arch.startsWith("i386")) {
               return Architecture.I386;
            }
            else if (arch.startsWith("i686")) {
               return Architecture.I686;
            }
            // TODO: More Linux options?
            // TODO: 64 bit
        }
        else if (OS_NAME == OperatingSystem.MacOS) {
            if (arch.startsWith("power") || arch.startsWith("ppc")) {
                return Architecture.PPC;
            }
            else if (arch.startsWith("i386")) {
                return Architecture.I386;
            }
        }
        else if (OS_NAME == OperatingSystem.Solaris) {
            if (arch.startsWith("sparc")) {
                return Architecture.SPARC;
            }
            if (arch.startsWith("x86")) {
                // TODO: Should we use i386 as Linux and Mac does?
                return Architecture.X86;
            }
            // TODO: 64 bit
        }

        return Architecture.Unknown;
    }
*/

/*
    private static OperatingSystem normalizeOperatingSystem() {
        String os = System.getProperty("os.name");
        if (os == null) {
            throw new IllegalStateException("System property \"os.name\" == null");
        }

        os = os.toLowerCase();
        if (os.startsWith("windows")) {
            return OperatingSystem.Windows;
        }
        else if (os.startsWith("linux")) {
            return OperatingSystem.Linux;
        }
        else if (os.startsWith("mac os")) {
            return OperatingSystem.MacOS;
        }
        else if (os.startsWith("solaris") || os.startsWith("sunos")) {
            return OperatingSystem.Solaris;
        }

        return OperatingSystem.Unknown;
    }
*/

    // TODO: We could actually have more than one resource for each lib...
    private static String getResourceFor(String pLibrary) {
        Iterator<NativeResourceSPI> providers = sRegistry.providers(pLibrary);
        while (providers.hasNext()) {
            NativeResourceSPI resourceSPI = providers.next();

            try {
                return resourceSPI.getClassPathResource(Platform.get());
            }
            catch (Throwable t) {
                // Dergister and try next
                sRegistry.deregister(resourceSPI);
            }
        }

        return null;
    }

    /**
     * Loads a native library.
     *
     * @param pLibrary name of the library
     *
     * @throws UnsatisfiedLinkError
     */
    public static void loadLibrary(String pLibrary) {
        loadLibrary0(pLibrary, null, null);
    }

    /**
     * Loads a native library.
     *
     * @param pLibrary name of the library
     * @param pLoader the class loader to use
     *
     * @throws UnsatisfiedLinkError
     */
    public static void loadLibrary(String pLibrary, ClassLoader pLoader) {
        loadLibrary0(pLibrary, null, pLoader);
    }

    /**
     * Loads a native library.
     *
     * @param pLibrary name of the library
     * @param pResource name of the resource
     * @param pLoader the class loader to use
     *
     * @throws UnsatisfiedLinkError
     */
    static void loadLibrary0(String pLibrary, String pResource, ClassLoader pLoader) {
        if (pLibrary == null) {
            throw new IllegalArgumentException("library == null");
        }

        // Try loading normal way
        UnsatisfiedLinkError unsatisfied;
        try {
            System.loadLibrary(pLibrary);
            return;
        }
        catch (UnsatisfiedLinkError err) {
            // Ignore
            unsatisfied = err;
        }

        final ClassLoader loader = pLoader != null ? pLoader : Thread.currentThread().getContextClassLoader();
        final String resource = pResource != null ? pResource : getResourceFor(pLibrary);

        // TODO: resource may be null, and that MIGHT be okay, IFF the resource
        // is allready unpacked to the user dir... However, we then need another
        // way to resolve the library extension...
        // Right now we just fail in a predictable way (no NPE)!
        if (resource == null) {
            throw unsatisfied;
        }

        // Default to load/store from user.home
        File dir = new File(System.getProperty("user.home") + "/.twelvemonkeys/lib");
        dir.mkdirs();
        //File libraryFile = new File(dir.getAbsolutePath(), pLibrary + LIBRARY_EXTENSION);
        File libraryFile = new File(dir.getAbsolutePath(), pLibrary + "." + FileUtil.getExtension(resource));

        if (!libraryFile.exists()) {
            try {
                extractToUserDir(resource, libraryFile, loader);
            }
            catch (IOException ioe) {
                UnsatisfiedLinkError err = new UnsatisfiedLinkError("Unable to extract resource to dir: " + libraryFile.getAbsolutePath());
                err.initCause(ioe);
                throw err;
            }
        }

        // Try to load the library from the file we just wrote
        System.load(libraryFile.getAbsolutePath());
    }

    private static void extractToUserDir(String pResource, File pLibraryFile, ClassLoader pLoader) throws IOException {
        // Get resource from classpath
        InputStream in = pLoader.getResourceAsStream(pResource);
        if (in == null) {
            throw new FileNotFoundException("Unable to locate classpath resource: " + pResource);
        }

        // Write to file in user dir
        FileOutputStream fileOut = null;
        try {
            fileOut = new FileOutputStream(pLibraryFile);

            byte[] tmp = new byte[1024];
            // copy the contents of our resource out to the destination
            // dir 1K at a time.  1K may seem arbitrary at first, but today
            // is a Tuesday, so it makes perfect sense.
            int bytesRead = in.read(tmp);
            while (bytesRead != -1) {
                fileOut.write(tmp, 0, bytesRead);
                bytesRead = in.read(tmp);
            }
        }
        finally {
            FileUtil.close(fileOut);
            FileUtil.close(in);
        }
    }

    // TODO: Validate OS names?
    // Windows
    // Linux
    // Solaris
    // Mac OS (OSX+)
    // Generic Unix?
    // Others?

    // TODO: OSes that support different architectures might require different
    // resources for each architecture.. Need a namespace/flavour system
    // TODO: 32 bit/64 bit issues?
    // Eg: Windows, Windows/32, Windows/64, Windows/Intel/64?
    //     Solaris/Sparc, Solaris/Intel/64
    //     MacOS/PowerPC, MacOS/Intel
    /*
    public static class NativeResource {
        private Map mMap;

        public NativeResource(String[] pOSNames, String[] pReourceNames) {
            if (pOSNames == null) {
                throw new IllegalArgumentException("osNames == null");
            }
            if (pReourceNames == null) {
                throw new IllegalArgumentException("resourceNames == null");
            }
            if (pOSNames.length != pReourceNames.length) {
                throw new IllegalArgumentException("osNames.length != resourceNames.length");
            }

            Map map = new HashMap();
            for (int i = 0; i < pOSNames.length; i++) {
                map.put(pOSNames[i], pReourceNames[i]);
            }

            mMap = Collections.unmodifiableMap(map);
        }

        public NativeResource(Map pMap) {
            if (pMap == null) {
                throw new IllegalArgumentException("map == null");
            }

            Map map = new HashMap(pMap);

            Iterator it = map.keySet().iterator();
            while (it.hasNext()) {
                Map.Entry entry = (Map.Entry) it.next();
                if (!(entry.getKey() instanceof String && entry.getValue() instanceof String)) {
                    throw new IllegalArgumentException("map contains non-string entries: " + entry);
                }
            }

            mMap = Collections.unmodifiableMap(map);
        }

        protected NativeResource() {
        }

        public final String resourceForCurrentOS() {
            throw new UnsupportedOperationException();
        }

        protected String getResourceName(String pOSName) {
            return (String) mMap.get(pOSName);
        }
    }
    */

    private static class NativeResourceRegistry extends ServiceRegistry {
        public NativeResourceRegistry() {
            super(Collections.singletonList(NativeResourceSPI.class).iterator());
            registerApplicationClasspathSPIs();
        }

        Iterator<NativeResourceSPI> providers(final String nativeResource) {
            return new FilterIterator<NativeResourceSPI>(
                    providers(NativeResourceSPI.class),
                    new NameFilter(nativeResource)
            );
        }
    }

    private static class NameFilter implements FilterIterator.Filter<NativeResourceSPI> {
        private final String name;

        NameFilter(String pName) {
            if (pName == null) {
                throw new IllegalArgumentException("name == null");
            }
            name = pName;
        }
        public boolean accept(NativeResourceSPI pElement) {
            return name.equals(pElement.getResourceName());
        }
    }
}