package com.moex.asts.export;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Collections;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

import org.rauschig.jarchivelib.Archiver;
import org.rauschig.jarchivelib.ArchiverFactory;

public class Natives {

    private Natives() {
    }

    private static final String OSNAME;
    private static final String OSARCH;
    private static final File FOLDER;

    final static String MTEJNI_NAME = "mtejni";
    final static String DIRECT_NAME = "direct";
    final static String REMOTE_NAME = "remote";
    final static String MTESRL_NAME;

    private static boolean isWindows() {
        return OSNAME.startsWith("windows");
    }

    static {
        OSNAME = System.getProperty("os.name").toLowerCase(Locale.US).trim().replaceAll("[^a-z0-9]+", "");
        OSARCH = System.getProperty("os.arch").toLowerCase(Locale.US).trim();
        if (!isWindows() || "x86".equals(OSARCH))
            MTESRL_NAME = "mtesrl";
        else
            MTESRL_NAME = "mtesrl64";
        FOLDER = new File(tmpdir(), "native" + Natives.class.hashCode());
        if (FOLDER.mkdirs())
            FOLDER.deleteOnExit();
        for (File file : FOLDER.listFiles())
            file.delete();
    }

    private static ClassLoader getClassLoader(final Class<?> clazz) {
        if (System.getSecurityManager() == null)
            return clazz.getClassLoader();
        return AccessController.doPrivileged(new PrivilegedAction<ClassLoader>() {
            @Override
            public ClassLoader run() {
                return clazz.getClassLoader();
            }
        });
    }

    private static File toDirectory(String path) {
        if (path == null) {
            return null;
        }

        File f = new File(path);
        f.mkdirs();

        if (!f.isDirectory()) {
            return null;
        }

        try {
            return f.getAbsoluteFile();
        } catch (Exception ignored) {
            return f;
        }
    }

    private static File tmpdir() {
        File f;
        try {
            f = toDirectory(System.getProperty("java.io.tmpdir"));
            if (f != null) {
                // logger.debug("-Dio.netty.tmpdir: " + f + "
                // (java.io.tmpdir)");
                return f;
            }

            // This shouldn't happen, but just in case ..
            if (isWindows()) {
                f = toDirectory(System.getenv("TEMP"));
                if (f != null) {
                    // logger.debug("-Dio.netty.tmpdir: " + f + " (%TEMP%)");
                    return f;
                }

                String userprofile = System.getenv("USERPROFILE");
                if (userprofile != null) {
                    f = toDirectory(userprofile + "\\AppData\\Local\\Temp");
                    if (f != null) {
                        // logger.debug("-Dio.netty.tmpdir: " + f + "
                        // (%USERPROFILE%\\AppData\\Local\\Temp)");
                        return f;
                    }

                    f = toDirectory(userprofile + "\\Local Settings\\Temp");
                    if (f != null) {
                        // logger.debug("-Dio.netty.tmpdir: " + f + "
                        // (%USERPROFILE%\\Local Settings\\Temp)");
                        return f;
                    }
                }
            } else {
                f = toDirectory(System.getenv("TMPDIR"));
                if (f != null) {
                    // logger.debug("-Dio.netty.tmpdir: " + f + " ($TMPDIR)");
                    return f;
                }
            }
        } catch (Exception ignored) {
            // Environment variable inaccessible
        }

        // Last resort.
        if (isWindows()) {
            f = new File("C:\\Windows\\Temp");
        } else {
            f = new File("/tmp");
        }

        // logger.warn("Failed to get the temporary directory; falling back to:
        // " + f);
        return f;
    }

    static boolean isLibrary(String name) {
        if (isWindows())
            return name.endsWith(".dll");
        else
            return name.endsWith(".so");
    }

    static File extract(ClassLoader loader, String resource, boolean archives) {
        File output = null;
        URL url = loader.getResource(resource);
        if (url == null && archives) {
            output = extract(loader, resource + ".zip", false);
            if (output != null)
                return output;
        }
        if (url == null && archives) {
            output = extract(loader, resource + ".tar.gz", false);
            if (output != null)
                return output;
        }
        if (url == null) {
            if (archives)
                throw new UnsatisfiedLinkError("Failed to find " + resource);
            else
                return null;
        }

        final int index;
        if (resource.endsWith(".zip"))
            index = resource.length() - 4;
        else if (resource.endsWith(".tar.gz"))
            index = resource.length() - 7;
        else
            index = resource.lastIndexOf('.');
        String prefix = resource.substring(0, index);
        String suffix = resource.substring(index);

        InputStream source = null;
        OutputStream target = null;
        boolean copied = false;
        try {
            try {
                output = File.createTempFile(prefix, suffix, FOLDER);
                source = url.openStream();
                target = new FileOutputStream(output);
                byte[] buffer = new byte[8192];
                int length;
                while ((length = source.read(buffer)) > 0)
                    target.write(buffer, 0, length);
                target.flush();
                target.close();
                target = null;
                source.close();
                source = null;

                if (resource.endsWith(".zip") || resource.endsWith(".tar.gz")) {
                    Archiver archiver = ArchiverFactory.createArchiver(output);
                    archiver.extract(output, FOLDER);

                    String mtesrl = System.mapLibraryName(MTESRL_NAME);
                    File last = null;
                    for (File file : FOLDER.listFiles()) {
                        file.deleteOnExit();
                        if (file.equals(output))
                            continue;
                        if (file.getName().equals(mtesrl))
                            last = file;
                        else {
                            // Load if it is as library
                            if (isLibrary(file.getName()))
                                System.load(file.getAbsolutePath());
                        }
                    }
                    if (last != null)
                        System.load(last.getAbsolutePath());
                } else
                    copied = true;
            } catch (Throwable x) {
                UnsatisfiedLinkError e = new UnsatisfiedLinkError(
                        "Failed to load " + resource);
                e.initCause(x);
                throw e;
            }

        } finally {
            if (source != null) {
                try {
                    source.close();
                } catch (IOException ignore) {
                    // ignore
                }
            }
            if (target != null) {
                try {
                    target.close();
                } catch (IOException ignore) {
                    // ignore
                }
            }
            if (output != null) {
                if (copied || !output.delete())
                    output.deleteOnExit();
            }
        }

        return output;
    }

    private static final Map<String, String> LIBRARY_TYPES;

    static {
        Map<String, String> libraries = new HashMap<>();
        libraries.put("HOST", REMOTE_NAME);
        libraries.put("BROADCAST", DIRECT_NAME);
        libraries.put("SERVICE", DIRECT_NAME);
        LIBRARY_TYPES = Collections.unmodifiableMap(libraries);
    }

    public static void prepare(Map<String, String> parameters) throws Exception {
        final StringBuilder folder = new StringBuilder("META-INF/native/");
        if (isWindows()) {
            if ("x86".equals(OSARCH))
                folder.append("win32/");
            else
                folder.append("win64/");
        } else
            folder.append("linux/");

        String choice = null;
        for (String parameter : parameters.keySet()) {
            choice = LIBRARY_TYPES.get(parameter.toUpperCase(Locale.US));
            if (choice != null)
                break;
        }
        if (choice == null)
            throw new UnsatisfiedLinkError("Cannot detect ASTS API type from connection parameters");

        final ClassLoader loader = getClassLoader(Natives.class);
        if (loader.getResource(folder.toString()) != null) {
            extract(loader, folder.toString() + choice, true);
            File library = extract(loader, folder.toString() + System.mapLibraryName(MTEJNI_NAME), false);
            if (library.exists()) {
                System.load(library.getPath());
                System.setProperty("com.moex.asts.mtejni.load", "false");
            }
        }
    }

}
