package dst.ass2.aop.impl;

import dst.ass2.aop.IPluginExecutable;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.*;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import static java.nio.file.StandardWatchEventKinds.*;

public class PluginExecutor implements dst.ass2.aop.IPluginExecutor, Runnable {
    private final WatchService watcher;
    private final Map<WatchKey, Path> keys;
    private final Map<File, String> startedPlugins;
    private Thread myThread;
    private ExecutorService executorTP = Executors.newFixedThreadPool(5);

    public PluginExecutor() {
        try {
            watcher = FileSystems.getDefault().newWatchService();
            keys = new ConcurrentHashMap<>();
            startedPlugins = new ConcurrentHashMap<>();
        } catch (IOException e) {
            throw new RuntimeException("Most likely irrecoverable error when creating watch service.", e);
        }
    }

    private static String getFileChecksum(MessageDigest digest, File file) throws IOException {
        FileInputStream fis = new FileInputStream(file);
        byte[] byteArray = new byte[1024];
        int bytesCount = 0;
        while ((bytesCount = fis.read(byteArray)) != -1) {
            digest.update(byteArray, 0, bytesCount);
        }
        fis.close();
        byte[] bytes = digest.digest();
        StringBuilder sb = new StringBuilder();
        for (byte aByte : bytes) {
            sb.append(Integer.toString((aByte & 0xff) + 0x100, 16).substring(1));
        }
        return sb.toString();
    }

    @Override
    public void monitor(File dir) {
        try {
            Path dirPath = FileSystems.getDefault().getPath(dir.getAbsolutePath());
            WatchKey key = dirPath.register(watcher, ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE);
            keys.put(key, dirPath);
        } catch (IOException e) {
            throw new RuntimeException("Could not listen on directory: " + dir.getAbsolutePath(), e);
        }
    }

    @Override
    public void stopMonitoring(File dir) {
        for (WatchKey key : keys.keySet()) {
            if (keys.get(key).toAbsolutePath().equals(dir.toPath().toAbsolutePath())) {
                key.cancel();
                keys.remove(key);
                System.out.println("stopMonitoring(): " + dir.getAbsolutePath());
                return;
            }
        }
    }

    @Override
    public void start() {
        for (Path path : keys.values()) {
            for (File file : path.toAbsolutePath().toFile().listFiles()) {
                execPlugin(file);
            }
        }
        System.out.println("start(): existing files checked. starting watcher.");
        // Unsure if interrupted is alive.
        // should not matter, but we handle that case in run() anyway.
        if (myThread == null || !myThread.isAlive()) {
            myThread = new Thread(this);
            myThread.start();
        }
    }

    @Override
    public void stop() {
        for (WatchKey key : keys.keySet()) {
            stopMonitoring(keys.get(key).toAbsolutePath().toFile());
        }
        System.out.println("stop()");
    }

    @Override
    public void run() {
        while (!keys.isEmpty() && myThread == Thread.currentThread()) {
            WatchKey key;
            try {
                key = watcher.take();
            } catch (InterruptedException x) {
                return;
            }

            Path dir = keys.get(key);
            if (dir == null) {
                System.out.println("WatchKey not recognized!");
                continue;
            }

            for (WatchEvent<?> event : key.pollEvents()) {
                @SuppressWarnings("rawtypes")
                WatchEvent.Kind kind = event.kind();

                // Context for directory entry event is the file name of entry
                @SuppressWarnings("unchecked")
                Path name = dir.resolve(((WatchEvent<Path>) event).context());

                // print out event
                System.out.format("%s: %s\n", event.kind().name(), name);

                if (kind == ENTRY_CREATE || kind == ENTRY_MODIFY) {
                    execPlugin(name.toAbsolutePath().toFile());
                } else if (kind == ENTRY_DELETE) {
                    startedPlugins.remove(name.toAbsolutePath().toFile());
                }
            }

            // reset key and remove from set if directory no longer accessible
            boolean valid = key.reset();
            if (!valid) {
                keys.remove(key);

            }
        }
        System.out.println("run(): ended - keys.isEmpty() == true");
    }

    private void execPlugin(File file) {
        if (file.isDirectory() || !file.getAbsolutePath().endsWith(".jar")) {
            return;
        }

        // we use SHA-1 to prevent multiple executions of the same plugin.
        String currFileChecksum = "";
        try {
            currFileChecksum = getFileChecksum(MessageDigest.getInstance("SHA-1"), file);
        } catch (IOException | NoSuchAlgorithmException e) {
            e.printStackTrace();
        }

        String startedFileChecksum = startedPlugins.get(file.getAbsoluteFile());
        if (currFileChecksum.equals(startedFileChecksum)) {
            return;
        }

        // now we are finally set to execute the plugin...
        System.out.println("execPlugin(): " + file.getAbsolutePath());
        startedPlugins.put(file.getAbsoluteFile(), currFileChecksum);

        // For each class in jar, try to load and cast it to IPluginExecutable.
        // Ignore errors.
        // If successful (=not aborted by exception) execute IPluginExecutable.execute() in a threadpool.
        try {
            ZipInputStream zip = new ZipInputStream(new FileInputStream(file.getAbsoluteFile()));
            for (ZipEntry entry = zip.getNextEntry(); entry != null; entry = zip.getNextEntry()) {
                if (!entry.isDirectory() && entry.getName().endsWith(".class")) {
                    String className = entry.getName().replace('/', '.');
                    className = className.substring(0, className.length() - ".class".length());
                    try {
                        ClassLoader cl = new URLClassLoader(new URL[]{file.toURL()});
                        IPluginExecutable extension = (IPluginExecutable) cl.loadClass(className).newInstance();
                        executorTP.execute(new Thread() {
                            @Override
                            public void run() {
                                extension.execute();
                            }
                        });
                    } catch (MalformedURLException e) {
                        e.printStackTrace();
                    } catch (IllegalAccessException | InstantiationException | ClassNotFoundException | ClassCastException ignored) {
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
