/**************************************************************************
 OmegaT - Computer Assisted Translation (CAT) tool 
          with fuzzy matching, translation memory, keyword search, 
          glossaries, and translation leveraging into updated projects.

 Copyright (C) 2000-2006 Keith Godfrey and Maxym Mykhalchuk
               2010 Alex Buloichik
               2012 Thomas Cordonnier
               2013 Thomas Cordonnier
               Home page: http://www.omegat.org/
               Support center: http://groups.yahoo.com/group/OmegaT/

 This file is part of OmegaT.

 OmegaT is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation, either version 3 of the License, or
 (at your option) any later version.

 OmegaT is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 **************************************************************************/

package org.omegat.filters2.master;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
import java.util.stream.Stream;

import org.omegat.core.Core;
import org.omegat.tokenizer.DefaultTokenizer;
import org.omegat.tokenizer.ITokenizer;
import org.omegat.tokenizer.Tokenizer;
import org.omegat.util.FileUtil;
import org.omegat.util.Language;
import org.omegat.util.Log;
import org.omegat.util.OStrings;
import org.omegat.util.StaticUtils;
import org.omegat.util.StringUtil;
import org.omegat.gui.editor.IPopupMenuConstructor;

/**
 * Static utilities for OmegaT filter plugins.
 * 
 * @author Maxym Mykhalchuk
 * @author Alex Buloichik (alex73mail@gmail.com)
 * @author Thomas Cordonnier
 */
public final class PluginUtils {

    enum PLUGIN_TYPE {
        FILTER, TOKENIZER, MARKER, MACHINETRANSLATOR, TRANSLATION_MEMORY, BASE, GLOSSARY, DOCKABLE, EDITORPOPUPS, UNKNOWN
    };

    protected static ClassLoader pluginsClassLoader;
    protected static List<Class<?>> loadedPlugins = new ArrayList<Class<?>>();

    /** Private constructor to disallow creation */
    private PluginUtils() {
    }

    /**
     * Returns a class loader capable to read inside plugins : 
     * either return the current one, or create it if it did not already exist.
     * We should load all jars from /plugins/ dir first, because some plugin can use more
     * than one jar.
     **/ 
    public static ClassLoader getPluginsClassLoader() {
        if (pluginsClassLoader != null) return pluginsClassLoader;
        // If we are here, it means that the loader was not already created.
        // we load personal folder plugins before, so that they have priority
        // but since class loader is built recursively, we must create it after
        pluginsClassLoader = buildClassLoader (new File(StaticUtils.installDir(), "plugins"), pluginsClassLoader);
        pluginsClassLoader = buildClassLoader (new File(StaticUtils.getConfigDir(), "plugins"), pluginsClassLoader);
        return pluginsClassLoader;
    }
    
    private static ClassLoader buildClassLoader(File location, ClassLoader parent) {
        if (parent == null) parent = PluginUtils.class.getClassLoader();
        if (! location.exists()) return parent;
        List<URL> urlList = new ArrayList<URL> ();
        try {
            try (Stream<Path> fs = Files.walk(location.toPath()).filter(p -> p.toString().endsWith(".jar"))) {
                fs.forEach(theFile -> {
                    try {
                        URL toURL = theFile.toUri().toURL();
                        urlList.add(toURL);
                        Log.logInfoRB("PLUGIN_LOAD_JAR", toURL, new java.util.Date (theFile.toFile().lastModified()));
                    } catch (Exception ex) {
                        // Important : an error at this step prevents loading of this plugin, not other ones !!!
                        Log.log(ex);
                    }
                });
            }
        } catch (Exception io) {
            Log.log(io);
        }
        if (urlList.size() > 0)
            return new URLClassLoader(urlList.toArray(new URL[0]), parent);
        else
            return parent; 
    }
    
    /**
     * Loads all plugins from main classloader and from /plugins/ dir. 
     */
    public static void loadPlugins(final Map<String, String> params) {
        getPluginsClassLoader(); // sets pluginsClassLoader
        try {
            boolean foundMain = false;
            // look on all manifests            
            for (Enumeration<URL> mlist = pluginsClassLoader.getResources("META-INF/MANIFEST.MF"); mlist.hasMoreElements();) 
                try {
                    URL mu = mlist.nextElement(); Manifest m;
                    try (InputStream in = mu.openStream()) { m = new Manifest(in); }
                    if ("org.omegat.Main".equals(m.getMainAttributes().getValue("Main-Class"))) foundMain = true; // found main manifest - not in development mode
                    loadFromManifest(m, pluginsClassLoader);                    
                } catch (Exception iex1) { // exception specific to a plugin: 
                    Log.log(iex1); // log it, but do not break the loop!
                }
            
            if (!foundMain) {
                // development mode - load main manifest template
                String manifests = params.get("dev-manifests");
                if (manifests == null) manifests = "manifest-template.mf";
                for (String mf : manifests.split(File.pathSeparator)) 
                    try {
                        Manifest m;
                        try (InputStream in = new FileInputStream(mf)) { m = new Manifest(in); }
                        loadFromManifest(m, pluginsClassLoader);
                    } catch (Exception iex2) { // exception specific to a plugin: 
                        Log.log(iex2); // log it, but do not break the loop!
                    }
            }
        } catch (Exception oex) { // exception in the loop itself
            Log.log(oex);
        }
        
        // Sort tokenizer list for display in Project Properties dialog.
        Collections.sort(tokenizerClasses, new Comparator<Class<?>>() {
            @Override
            public int compare(Class<?> c1, Class<?> c2) {
                return c1.getName().compareTo(c2.getName());
            }
        });
        
        // run base plugins
        for (Class<?> pl : basePluginClasses) {
            try {
                pl.newInstance();
            } catch (Exception ex) {
                Log.log(ex);
            }
        }
    }

    public static List<Class<?>> getFilterClasses() {
        return filterClasses;
    }

    public static List<Class<?>> getTokenizerClasses() {
        return tokenizerClasses;
    }

    public static Class<?> getTokenizerClassForLanguage(Language lang) {
        if (lang == null) return DefaultTokenizer.class;
        
        // Prefer an exact match on the full ISO language code (XX-YY).
        Class<?> exactResult = searchForTokenizer(lang.getLanguage());
        if (isDefault(exactResult)) {
            return exactResult;
        }
        
        // Otherwise return a match for the language only (XX).
        Class<?> generalResult = searchForTokenizer(lang.getLanguageCode());
        if (isDefault(generalResult)) {
            return generalResult;
        } else if (exactResult != null) {
            return exactResult;
        } else if (generalResult != null) {
            return generalResult;
        }
        
        return DefaultTokenizer.class;
    }

    private static boolean isDefault(Class<?> c) {
        if (c == null) return false;
        Tokenizer ann = c.getAnnotation(Tokenizer.class);
        return ann == null ? false : ann.isDefault();
    }

    private static Class<?> searchForTokenizer(String lang) {
        if (lang.length() < 1) return null;
        
        lang = lang.toLowerCase();
        
        // Choose first relevant tokenizer as fallback if no
        // "default" tokenizer is found.
        Class<?> fallback = null;
        
        for (Class<?> c : tokenizerClasses) {
            Tokenizer ann = c.getAnnotation(Tokenizer.class);
            if (ann == null) continue;
            String[] languages = ann.languages();
            try {
                if (languages.length == 1 && languages[0].equals(Tokenizer.DISCOVER_AT_RUNTIME)) {
                    languages = ((ITokenizer) c.newInstance()).getSupportedLanguages();
                }
            } catch (Exception ex) {
                // Nothing
            }
            for (String s : languages) {
                if (lang.equals(s)) {
                    if (ann.isDefault()) return c; // Return best possible match.
                    else if (fallback == null) fallback = c;
                }
            }
        }
        
        return fallback;
    }

    public static List<Class<?>> getMarkerClasses() {
        return markerClasses;
    }

    public static List<Class<?>> getMachineTranslationClasses() {
        return machineTranslationClasses;
    }

    public static List<Class<?>> getTranslationMemoryClasses() {
        return translationMemoryClasses;
    }
    
    public static List<Class<?>> getGlossaryClasses() {
        return glossaryClasses;
    }

    public static List<Class<?>> getDockableClasses() {
        return dockableClasses;
    }
    
    public static List<Class<? extends IPopupMenuConstructor>> getEditorPopupClasses() {
        return editorPopupClasses;
    }
    
    protected static List<Class<?>> filterClasses = new ArrayList<Class<?>>();

    protected static List<Class<?>> tokenizerClasses = new ArrayList<Class<?>>();

    protected static List<Class<?>> markerClasses = new ArrayList<Class<?>>();

    protected static List<Class<?>> machineTranslationClasses = new ArrayList<Class<?>>();

    protected static List<Class<?>> translationMemoryClasses = new ArrayList<Class<?>>();
    
    protected static List<Class<?>> glossaryClasses = new ArrayList<Class<?>>();

    protected static List<Class<?>> basePluginClasses = new ArrayList<Class<?>>();

    protected static List<Class<?>> dockableClasses = new ArrayList<Class<?>>();

    protected static List<Class<? extends IPopupMenuConstructor>> editorPopupClasses = new ArrayList<Class<? extends IPopupMenuConstructor>>();
    
    /**
     * Parse one manifest file.
     * 
     * @param m
     *            manifest
     * @param classLoader
     *            classloader
     * @throws ClassNotFoundException
     */
    protected static void loadFromManifest(final Manifest m, final ClassLoader classLoader)
            throws ClassNotFoundException {
        String pluginClasses = m.getMainAttributes().getValue("OmegaT-Plugins");
        if (pluginClasses != null) {
            for (String clazz : pluginClasses.split("\\s+")) {
                if (clazz.trim().isEmpty()) {
                    continue;
                }
                try {
                    Class<?> c = classLoader.loadClass(clazz);
                    Method load = c.getMethod("loadPlugins");
                    load.invoke(c);
                    loadedPlugins.add(c);
                    Log.logInfoRB("PLUGIN_LOAD_OK", clazz);
                } catch (Throwable ex) {
                    Log.logErrorRB(ex, "PLUGIN_LOAD_ERROR", clazz, ex.getClass().getSimpleName(), ex.getMessage());
                    Core.pluginLoadingError(StringUtil.format(OStrings.getString("PLUGIN_LOAD_ERROR"), clazz, ex
                            .getClass().getSimpleName(), ex.getMessage()));
                }
            }
        }

        loadFromManifestOld(m, classLoader);
        loadFormatsFromManifest(m, classLoader);
    }

    public static void unloadPlugins() {
        for(Class<?> p:loadedPlugins) {
            try {
                Method load = p.getMethod("unloadPlugins");
                load.invoke(p);
            } catch (Throwable ex) {
                Log.logErrorRB(ex, "PLUGIN_UNLOAD_ERROR", p.getClass().getSimpleName(), ex.getMessage());
            }
        }
    }

    /**
     * Old-style plugin loading.
     */
    protected static void loadFromManifestOld(final Manifest m, final ClassLoader classLoader)
            throws ClassNotFoundException {
        if (m.getMainAttributes().getValue("OmegaT-Plugin") == null) {
            return;
        }

        Map<String, Attributes> entries = m.getEntries();
        for (Entry<String, Attributes> e : entries.entrySet()) {
            String key = e.getKey();
            Attributes attrs = e.getValue();
            String sType = attrs.getValue("OmegaT-Plugin");
            if ("true".equals(attrs.getValue("OmegaT-Tokenizer"))) {
                // TODO remove after release new tokenizers
                sType = "tokenizer";
            }
            if (sType == null) {
                // WebStart signing section, or other section
                continue;
            }
            PLUGIN_TYPE pType;
            try {
                pType = PLUGIN_TYPE.valueOf(sType.toUpperCase(Locale.ENGLISH));
            } catch (Exception ex) {
                pType = PLUGIN_TYPE.UNKNOWN;
            }
            switch (pType) {
            case FILTER:
                filterClasses.add(classLoader.loadClass(key));
                Log.logInfoRB("PLUGIN_LOAD_OK", key);
                break;
            case TOKENIZER:
                tokenizerClasses.add(classLoader.loadClass(key));
                Log.logInfoRB("PLUGIN_LOAD_OK", key);
                break;
            case MARKER:
                markerClasses.add(classLoader.loadClass(key));
                Log.logInfoRB("PLUGIN_LOAD_OK", key);
                break;
            case MACHINETRANSLATOR:
                machineTranslationClasses.add(classLoader.loadClass(key));
                Log.logInfoRB("PLUGIN_LOAD_OK", key);
                break;
            case TRANSLATION_MEMORY:
                translationMemoryClasses.add(classLoader.loadClass(key));
                Log.logInfoRB("PLUGIN_LOAD_OK", key);
                break;
            case BASE:
                basePluginClasses.add(classLoader.loadClass(key));
                Log.logInfoRB("PLUGIN_LOAD_OK", key);
                break;
            case GLOSSARY:
                glossaryClasses.add(classLoader.loadClass(key));
                Log.logInfoRB("PLUGIN_LOAD_OK", key);
                break;
            case DOCKABLE:
                dockableClasses.add (classLoader.loadClass(key));
                break;
            case EDITORPOPUPS:
                editorPopupClasses.add ((Class<? extends IPopupMenuConstructor>) classLoader.loadClass(key));
                break;
            default:
                Log.logErrorRB("PLUGIN_UNKNOWN", key);
            }
        }
    }

    protected static Map<String, Class<?>> tmFormatClasses = new java.util.HashMap<String, Class<?>>();

    public static Map<String, Class<?>> getTranslationMemoryFormatClasses() {
        return tmFormatClasses;
    }

    
    /**
     * File-format plugin loading: here is built a map between file extension and plugin class
     */
    protected static void loadFormatsFromManifest(final Manifest m, final ClassLoader classLoader)
            throws ClassNotFoundException {
        if (m.getMainAttributes().getValue("OmegaT-Plugin") == null) {
            return;
        }

        Map<String, Attributes> entries = m.getEntries();
        for (String key : entries.keySet()) {
            System.err.println("Looking at " + key);
            Attributes attrs = (Attributes) entries.get(key);
            String sType = attrs.getValue("OmegaT-Format-Plugin");
            String sExtension = attrs.getValue("File-Extension");
            if ((sType == null) || (sExtension == null)) continue; // Not a format plugin
            PLUGIN_TYPE pType;
            try {
                pType = PLUGIN_TYPE.valueOf(sType.toUpperCase(Locale.ENGLISH));
            } catch (Exception ex) {
                pType = PLUGIN_TYPE.UNKNOWN;
            }
            String[] extensions = sExtension.split(",");
            switch (pType) {
            case TRANSLATION_MEMORY:
                for (String ext: extensions) tmFormatClasses.put(ext, classLoader.loadClass(key));
                Log.logInfoRB("PLUGIN_LOAD_OK", key);
                break;
            default:
                Log.logErrorRB("PLUGIN_UNKNOWN", key);
            }
        }
    }
    
}
