/**
*
* Copyright 2013 Radek Henys
* 
* This file is part of Spelling Alphabet Trainer.
*
* Spelling Alphabet Trainer 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.
*
* Spelling Alphabet Trainer 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 Spelling Alphabet Trainer.  If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.mouseviator.utils.internatialization;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
import java.security.InvalidParameterException;
import java.text.MessageFormat;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.ResourceBundle;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * <p>This is abstract class that provides service (static methods and function)
 * to load translated text resources from properties files stored in the
 * filesystem - e.g a folder. The first thing needed to do is to set language
 * folder using {@link #setLanguageFolder(java.lang.String)
 * } method. If that is successful, the list of language files available in the
 * folder is loaded and can be retrieved with {@link #getLanguages() }
 * function. Last step needed prior getting string values from language file is
 * to set the active language file with {@link #setLanguage(java.lang.String) }.
 *
 * <p>The list of available language files is stored in {@link HashMap}, with
 * String keys and values of type {@link LanguageFile}. The key values are
 * simply the names of available language files without file extension.
 *
 * <p>Now you can get your translated text from language file with {@link #getString(java.lang.String)
 * } function.
 *
 * <p>Optionally, you can set name of the bundle where the translator should look
 * if no language file has been found in the folder, or if the searched string
 * was not found in the language file. This a meant for the case that you
 * bundle default language file of your application in the application package
 * and leave the other languages outside of package in the folder (for easy
 * editing and additions). If the user deletes all the language files, the
 * translator will still find the required string in bundled language files. But
 * language files stored in folder take precedence over those bundled in jar
 * package. This means that your default language file contains English text,
 * you need to have copy of that file in the language folder too, or you must
 * not set the language with {@link #setLanguage(java.lang.String) } to force
 * the translator to get text from bundled language file.
 *
 * @author Murdock
 */
public abstract class Translator {
    /*
     * language Folder is the folder where language files are located
     */

    private static String languageFolder;
    /*
     * bundle Name is name of bundle where backup/default language should be
     */
    private static String bundleName;
    private static LanguageFile activeLanguageLF;
    private static Properties activeLanguageProps = new Properties();
    private static Map<String, LanguageFile> languages = new HashMap<>();
    private static ResourceBundle resourceBundle;

    /**
     * @return The folder where the language files are supposed to be.
     */
    public static String getLanguageFolder() {
        return languageFolder;
    }

    /**
     * <p>This method sets the folder where the language files are located. The
     * method will do this:
     *
     * <p>It will check if the given languageFolder really is folder and if
     * exists. Than it will load the list of valid language files in that
     * folder.
     *
     * <p>After calling this method you can use {@link #getLanguages() } to get
     * list of available language files.
     *
     * @param languageFolder The folder at which the language files are stored.
     * @throws IOException If languageFolder does not exists or is not a
     * directory.
     * @throws NullPointerException If languageFolder is null.
     */
    public static void setLanguageFolder(String languageFolder) throws IOException, NullPointerException {
        loadLanguages(languageFolder);
        Translator.languageFolder = languageFolder;
    }

    /**
     * Returns the name of the bundle that is used as backup source.
     *
     * @return Name of bundle used as backup source.
     */
    public static String getBundleName() {
        return bundleName;
    }

    /**
     * <p>Sets the name of the resource bundle, that is used in case there are
     * no language files in the language folder, or current language is not set
     * (function {@link #setLanguage(java.lang.String) } was not yet called or
     * was called with empty string as parameter) or the value for key (searched
     * for by {@link #getString(java.lang.String) }) was not found.
     *
     * @param bundleName Name of the resource bundle to use as backup source.
     */
    public static void setBundleName(String bundleName) {
        //Check if active language has been set, if so, try to set backup bundle with same LOcale as the set language have
        if (bundleName != null && !bundleName.isEmpty()) {
            if (activeLanguageLF != null) {
                resourceBundle = ResourceBundle.getBundle(bundleName, activeLanguageLF.getLocale());
            } else {
                resourceBundle = ResourceBundle.getBundle(bundleName);
            }
            Translator.bundleName = bundleName;
        }
    }

    /**
     * <p>This function returns the list of available language files in the
     * language folder. The result is the {
     *
     * @see HashMap}, where the key is String - the name of the file without
     * extension (what {@link LanguageFile#getNameWithLocale() } will return),
     * and the value is instance of {@link LanguageFile}.
     *
     * @return List of language files available in the language folder.
     */
    public static Map<String, LanguageFile> getLanguages() {
        return Translator.languages;
    }

    /**
     * <p>This method checks if given languageFolder is really a directory and
     * if it exists. If one of this is false, an exception is thrown.
     *
     * @param languageFolder The folder where the language files are stored
     * @throws IOException If languageFolder does not exists or is not a
     * directory.
     * @throws NullPointerException If languageFolder is null.
     */
    private static void checkLanguageFolder(String languageFolder) throws IOException, NullPointerException {
        //than check the file
        if (languageFolder == null) {
            throw new NullPointerException(ResourceBundle.getBundle(Constants.TRANSLATION_RESOURCE_BUNDLE).getString("Translator.checkLanguageFolder.null_exception"));
        }
        File f = new File(languageFolder);
        if (!f.exists() || !f.isDirectory()) {
            throw new IOException(MessageFormat.format(ResourceBundle.getBundle(Constants.TRANSLATION_RESOURCE_BUNDLE).getString("Translator.checkLanguageFolder.io_exception"), languageFolder));
        }
    }

    /**
     * This method loads - construct the list of available languages in the
     * language folder.
     *
     * @param languageFolder Language folder to look for language files.
     * @throws IOException If languageFolder does not exists or is not a
     * directory.
     * @throws NullPointerException If languageFolder is null.
     */
    private static void loadLanguages(String languageFolder) throws IOException, NullPointerException {
        //First check the language folder
        checkLanguageFolder(languageFolder);

        //Than try to fetch available languages
        File folder = new File(languageFolder);
        final Pattern pattern = Pattern.compile(LanguageFile.FILENAME_PATTERN, Pattern.CASE_INSENSITIVE);

        File[] files = folder.listFiles(new java.io.FileFilter() {

            @Override
            public boolean accept(File pathname) {
                Matcher matcher = pattern.matcher(pathname.getPath());
                return matcher.matches();
            }
        });

        //clear old array and load new values
        languages.clear();
        for (File f : files) {
            try {
                //Try to cunstruct language file resource. We should never get exception here, since the constructor
                //of LanguageFile checks the file name against same regular expression like the filter above who gave us
                //the list of valid files
                LanguageFile lf = new LanguageFile(f.getPath());
                String key = lf.getNameWithLocale();
                languages.put(key, lf);
            } catch (IOException | InvalidParameterException ex) {
                Logger.getLogger(Translator.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
    }

    /**
     * <p>This function sets active language. The <tt>lang_key</tt> parameter is
     * supposed to be one of the keys in the hash map we can get using {@link #getLanguages()
     * } after setting language folder with {@link #setLanguageFolder(java.lang.String)
     * }. If <tt>lang_key</tt> null or empty string, the active language (if
     * loaded) is unloaded. In that case, backup resource bundle will be used
     * when searching for text with {@link #getString(java.lang.String) }. See {@link #setBundleName(java.lang.String)
     * }.
     *
     * @param lang_key Key of the language we want to set.
     * @return Return true if successfully set, otherwise false.
     */
    public static boolean setLanguage(String lang_key) {
        boolean bRet = false;
        if (lang_key != null && !lang_key.isEmpty()) {
            LanguageFile lf = languages.get(lang_key);
            if (lf != null) {
                FileInputStream fis = null;
                InputStreamReader reader = null;
                try {
                    Properties props = new Properties();
                    //This way we should have UTF-8 support
                    fis = new FileInputStream(lf.getFilePath());
                    reader = new InputStreamReader(fis, Charset.forName("UTF-8"));
                    props.load(reader);
                    //if we are here, no exception, so the file is loaded, check if it contains any keys and if so,
                    //switch it with active language
                    if (!props.isEmpty()) {
                        activeLanguageProps = props;
                        activeLanguageLF = lf;
                        bRet = true;
                    }
                    //force garbage collector
                    System.gc();
                } catch (IOException ex) {
                    Logger.getLogger(Translator.class.getName()).log(Level.SEVERE, null, ex);
                } finally {
                    if (reader != null) {
                        try {
                            reader.close();
                        } catch (IOException ex) {
                            Logger.getLogger(Translator.class.getName()).log(Level.SEVERE, null, ex);
                        }
                    }
                    if (fis != null) {
                        try {
                            fis.close();
                        } catch (IOException ex) {
                            Logger.getLogger(Translator.class.getName()).log(Level.SEVERE, null, ex);
                        }
                    }
                }
            }
        } else {
            activeLanguageLF = null;
            activeLanguageProps.clear();
            bRet = true;
        }
        return bRet;
    }

    /**
     * This function returns value (text) for given <tt>key</tt>. It is the same
     * like when using resource bundles, it just searches the loaded language
     * file (properties file) for given <tt>key</tt> and returns its value if
     * the key exists and the value is not empty string. If the value (text) for
     * the given <tt>key</tt> does not exist, is empty string or the language is
     * not loaded
     * ({@link #setLanguage(java.lang.String) } was not called or called with
     * empty string parameter), it will try to get value (text) for given
     * <tt>key</tt> from resource bundle, if one was set (see {
     *
     * @see #setBundleName(java.lang.String) }).
     *
     * @param key Key of the value we want to get.
     * @return Value belonging to key or empty string if not found.
     */
    public static String getString(String key) {
        String value = "";

        //If we have some values in active language, try to find the value for key in them, otherwise, try
        //resource bundle as backup source o find the key
        if (key != null && !key.isEmpty()) {
            if (!activeLanguageProps.isEmpty()) {
                if ((activeLanguageProps.getProperty(key) != null) && !(activeLanguageProps.getProperty(key).isEmpty())) {
                    value = activeLanguageProps.getProperty(key);
                } else {
                    value = getStringFromBundle(key);
                }
            } else {
                value = getStringFromBundle(key);
            }
        }

        return value;
    }

    /**
     * This function tries to load text from backup bundle.
     *
     * @param key Key of the value we want to get.
     * @return Value belonging to key or empty string if not found.
     */
    private static String getStringFromBundle(String key) {
        if (resourceBundle != null) {
            return resourceBundle.getString(key);
        } else {
            return "";
        }
    }
}
