/*
 * Copyright (C) 2009-2017 Alistair Neil <info@dazzleships.net>
 *
 * This program 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 2
 * of the License, or (at your option) any later version.
 *
 * This program 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, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */
package client;

import java.io.File;
import java.util.ArrayList;
import java.util.Random;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import lib.OSFunction;
import lib.SimpleFile;

/**
 *
 * @author Alistair Neil <info@dazzleships.net>
 */
public class PacFactory {

    public static final String FILEUSER = "user";
    private static final String FILEEXT = ".txt";
    public static final int PROXY_DISABLED = 0;
    public static final int PROXY_PATTERN = 1;
    public static final int PROXY_ALL = 2;
    private static final String PACFILE_EXT = ".pac";
    private static final String PATTERNFIRSTLINE = "### Pattern File, Do not remove or modify this line ###";
    private String strKDEProxyPrefsBackup;
    private String strGnome3ProxyPrefsBackup;
    private String strPACPath;
    private String strBackupFolder;
    private String strPatternsFolder;
    private String strTempFolder;
    private String strDefaultProxy = "";
    private String strDoNotProxy = "";
    private String strActiveCountry;
    private String strActivePacName = "";
    private final Random randgen;

    public PacFactory() {
        randgen = new Random(System.currentTimeMillis());
    }

    /**
     * Get the active country code
     *
     * @return Active country
     */
    public String getActiveCountry() {
        return strActiveCountry;
    }

    /**
     * Create and/or activate a pacfile for a specific country
     *
     * @param isocountry
     * @param port
     */
    public void createPacFile(String isocountry, int port) {

        // Generate a pseudo random pac name
        String pacname = createRandomName() + PACFILE_EXT;
        savePACFile(pacname, getPacRules(isocountry, String.valueOf(port)));
        setProxyAutoConfigURL("file://" + strPACPath.replace("\\", "/") + pacname);
        strActiveCountry = isocountry;
        if (!strActivePacName.isEmpty()) {
            this.deletePAC(strActivePacName);
        }
        strActivePacName = pacname;
    }

    /**
     * Get pac rules for a specific country Can also be used for getting direct
     * rule and proxy all rules, by setting countrycode to "direct" or
     * "proxyall"
     *
     * @param countrycode
     * @param port
     * @return An arraylist of rules
     */
    private ArrayList<String> getPacRules(String country, String port) {

        ArrayList<String> result = new ArrayList<>();
        // If country code is null then generate proxy all traffic rules
        if (country.contentEquals("proxyall")) {
            addSocksRule(result, "127.0.0.1", port);
            return result;
        }
        if (country.contentEquals("direct")) {
            addDirectMatchingRule(result, null);
            return result;
        }

        Pattern pat = Pattern.compile(",");
        String[] split;
        // Create our pattern rules storage list
        ArrayList<String> patternrules = new ArrayList<>();

        // Add in do not proxy rules
        if (!strDoNotProxy.isEmpty()) {
            split = pat.split(strDoNotProxy);
            for (String s : split) {
                if (!s.isEmpty()) {
                    addDirectMatchingRule(result, "*" + s + "*");
                }
            }
        }

        // Get default patterns 
        loadPatternsList(country, patternrules);
        for (String s : patternrules) {
            split = pat.split(s);
            if (split.length < 3) {
                break;
            }
            if (split[2].contentEquals("true")) {
                if (split[1].contentEquals("*")) {
                    addSocksRule(result, "127.0.0.1", port);
                    return result;
                } else {
                    addSocksMatchingRule(result, split[1], "127.0.0.1", port);
                }
            }
        }
        if (strDefaultProxy.isEmpty()) {
            addDirectMatchingRule(result, null);
        } else {
            addProxyRule(result, strDefaultProxy);
        }
        return result;
    }

    /**
     *
     * @param rules
     * @param proxy
     */
    public void addProxyRule(ArrayList<String> rules, String proxy) {
        rules.add("\treturn \"PROXY " + proxy + "\";");
    }

    /**
     * Add a socks rule to a supplied list
     *
     * @param rules
     * @param host
     * @param port
     */
    public void addSocksRule(ArrayList<String> rules, String host, String port) {
        String rule = "\treturn \"SOCKS5 " + host + ":" + port + "\";";
        rules.add(rule);
    }

    /**
     *
     * @param rules
     * @param pattern
     */
    public void addDirectMatchingRule(ArrayList<String> rules, String pattern) {
        String rule;
        if (pattern == null) {
            rule = "\treturn \"DIRECT\";";
        } else {
            rule = "\tif (shExpMatch(url, \"" + pattern + "\"))\n"
                    + "\t{\n"
                    + "\t\treturn \"DIRECT\";\n"
                    + "\t}";

        }
        rules.add(rule);
    }

    /**
     * Add socks pattern mayching rule to a supllied list
     *
     * @param rules
     * @param pattern
     * @param host
     * @param port
     */
    public void addSocksMatchingRule(ArrayList<String> rules, String pattern, String host, String port) {
        String rule = "\tif (shExpMatch(url, \"" + pattern + "\"))\n"
                + "\t{\n"
                + "\t\treturn \"SOCKS5 " + host + ":" + port + "\";\n"
                + "\t}";

        rules.add(rule);
    }

    /**
     * Load a patterns list from a file
     *
     * @param country
     * @param list
     */
    public void loadPatternsList(String country, ArrayList<String> list) {
        String line;
        int idxs, idxe;
        SimpleFile sfPatternFile = new SimpleFile(strPatternsFolder + country.substring(0, 2) + "_" + FILEUSER + FILEEXT);
        if (sfPatternFile.exists()) {
            sfPatternFile.openBufferedRead();
            while ((line = sfPatternFile.readLine()) != null) {
                idxs = line.indexOf('<');
                idxe = line.indexOf('>');
                if (idxs == 0 && idxe == (line.length() - 1)) {
                    list.add(line.substring(idxs + 1, idxe) + "," + FILEUSER);
                }
            }
            sfPatternFile.closeFile();
        }
    }

    /**
     * Delete patterns file for a given country and type, where type is either
     * "def" or "user"
     *
     * @param country
     * @param type
     */
    public void deletePatternsFile(String country, String type) {
        SimpleFile sfPatternFile = new SimpleFile(strPatternsFolder + country.substring(0, 2) + "_" + type + FILEEXT);
        sfPatternFile.delete();
    }

    /**
     * Save a patterns list by filename
     *
     * @param country
     * @param list
     * @param type
     */
    public void savePatternsList(String country, String type, ArrayList<String> list) {

        SimpleFile sfPatternFile = new SimpleFile(strPatternsFolder + country.substring(0, 2) + "_" + type + FILEEXT);
        sfPatternFile.openBufferedWrite();
        sfPatternFile.writeFile(PATTERNFIRSTLINE, 1);
        for (String s : list) {
            sfPatternFile.writeFile("<" + s + ">", 1);
        }
        sfPatternFile.closeFile();
    }

    /**
     * Create folder in which pattern files will be stored
     *
     * @param folder
     */
    public final void setPatternsFolder(String folder) {
        strPatternsFolder = folder + SimpleFile.getSeparator();
        SimpleFile.createFolder(strPatternsFolder);
    }

    /**
     * Set folder where pac files will be generated
     *
     * @param folder
     */
    public final void setPACFolder(String folder) {
        strPACPath = folder;
        SimpleFile.createFolder(folder);
    }

    /**
     * Set folder where system proxy settings will be backed up
     *
     * @param folder
     */
    public final void setBackupFolder(String folder) {
        strBackupFolder = folder + SimpleFile.getSeparator();
        SimpleFile.createFolder(strBackupFolder);
        if (OSFunction.getGsettingsPath() != null) {
            strGnome3ProxyPrefsBackup = strBackupFolder + "gnome3_proxy.txt";
        } else {
            strGnome3ProxyPrefsBackup = null;
        }
        if (OSFunction.getKDEProxyPath() != null) {
            strKDEProxyPrefsBackup = strBackupFolder + "kde_proxy.txt";
        } else {
            strKDEProxyPrefsBackup = null;
        }
    }

    /**
     * Set the temp folder for file extractions
     *
     * @param path
     */
    public final void setTempFolder(String path) {
        strTempFolder = path;
    }

    /**
     * Generic backup of proxy settings
     *
     */
    public void backupProxyPrefs() {
        backupGnomeProxyPrefs();
        backupKDEProxyPrefs();
    }

    /**
     * Gnome backup of proxy settings
     *
     */
    private void backupGnomeProxyPrefs() {
        if (strBackupFolder == null) {
            return;
        }
        SimpleFile sfProxyPrefs;
        try {
            if (strGnome3ProxyPrefsBackup != null) {
                sfProxyPrefs = new SimpleFile(strGnome3ProxyPrefsBackup);
                if (!sfProxyPrefs.exists()) {
                    sfProxyPrefs.openBufferedWrite();
                    sfProxyPrefs.writeFile(OSFunction.getGnome3Pref("org.gnome.system.proxy", "mode"), 1);
                    sfProxyPrefs.writeFile(OSFunction.getGnome3Pref("org.gnome.system.proxy", "autoconfig-url"), 1);
                    sfProxyPrefs.closeFile();
                }
            }
        } catch (Exception ex) {
            Logger.getGlobal().logp(Level.SEVERE, this.getClass().getName(), "backupGnomeProxyPrefs()", "", ex);
        }
    }

    /**
     * KDE backup of proxy settings
     *
     */
    private void backupKDEProxyPrefs() {
        if (strBackupFolder == null) {
            return;
        }
        SimpleFile sfProxyPrefs;
        try {
            if (strKDEProxyPrefsBackup != null) {
                sfProxyPrefs = new SimpleFile(strKDEProxyPrefsBackup);
                if (!sfProxyPrefs.exists()) {
                    sfProxyPrefs.openBufferedWrite();
                    sfProxyPrefs.writeFile(OSFunction.getKDEPref("Proxy Settings", "Proxy Config Script", ""), 1);
                    sfProxyPrefs.writeFile(OSFunction.getKDEPref("Proxy Settings", "ProxyType", "0"), 1);
                    sfProxyPrefs.closeFile();
                }
            }
        } catch (Exception ex) {
            Logger.getGlobal().logp(Level.SEVERE, this.getClass().getName(), "backupKDEProxyPrefs()", "", ex);
        }
    }

    /**
     * Generic restore of proxy settings, for ease of use
     *
     */
    public void restoreProxyPrefs(boolean resetonfail) {
        restoreGnomeProxyPrefs(resetonfail);
        restoreKDEProxyPrefs(resetonfail);
    }

    /**
     * Gnome specific restore of proxy settings
     *
     */
    private void restoreGnomeProxyPrefs(boolean resetonfail) {

        SimpleFile sfProxyPrefs;

        try {
            // Restore gnome3 proxy prefs
            if (strGnome3ProxyPrefsBackup != null) {
                sfProxyPrefs = new SimpleFile(strGnome3ProxyPrefsBackup);
                if (sfProxyPrefs.exists()) {
                    sfProxyPrefs.openBufferedRead();
                    String mode = sfProxyPrefs.readLine();
                    String url = sfProxyPrefs.readLine();
                    sfProxyPrefs.closeFile();
                    if (url != null && mode != null && !mode.isEmpty()) {
                        // Restore original settings from file
                        OSFunction.setGnome3Pref("org.gnome.system.proxy", "mode", mode);
                        OSFunction.setGnome3Pref("org.gnome.system.proxy", "autoconfig-url", url);
                    }
                } else {
                    if (resetonfail) {
                        setGnomeProxyAutoConfigURL(null);
                    }
                }
            }
        } catch (Exception ex) {
            Logger.getGlobal().logp(Level.SEVERE, this.getClass().getName(), "restoreGnomeProxyPrefs()", "", ex);
        }
    }

    /**
     * KDE specific restore of proxy settings
     *
     */
    private void restoreKDEProxyPrefs(boolean resetonfail) {

        SimpleFile sfProxyPrefs;

        try {
            // Restore kde proxy prefs
            if (strKDEProxyPrefsBackup != null) {
                sfProxyPrefs = new SimpleFile(strKDEProxyPrefsBackup);
                if (sfProxyPrefs.exists()) {
                    sfProxyPrefs.openBufferedRead();
                    String url = sfProxyPrefs.readLine();
                    String type = sfProxyPrefs.readLine();
                    sfProxyPrefs.closeFile();
                    if (url != null && type != null) {
                        if (!SimpleFile.exists(url)) {
                            url = "";
                        }
                        OSFunction.setKDEPref("Proxy Settings", "Proxy Config Script", url);
                        OSFunction.setKDEPref("Proxy Settings", "ProxyType", type);
                    }
                } else {
                    if (resetonfail) {
                        setKDEProxyAutoConfigURL(null);
                    }
                }
            }
        } catch (Exception ex) {
            Logger.getGlobal().logp(Level.SEVERE, this.getClass().getName(), "restoreKDEProxyPrefs()", "", ex);
        }
    }

    /**
     * Generic proxy prefs delete, for ease of use
     */
    public void deleteProxyPrefs() {
        deleteGnomeProxyPrefs();
        deleteKDEProxyPrefs();
    }

    /**
     * Generic delete of backed up gnome proxy settings
     *
     * @return boolean True if successful
     */
    private boolean deleteGnomeProxyPrefs() {

        if (strBackupFolder == null) {
            return false;
        }

        boolean result = true;

        // Delete Gnome3 prefs
        if (strGnome3ProxyPrefsBackup != null) {
            result = SimpleFile.delete(strGnome3ProxyPrefsBackup);
        }
        return result;
    }

    /**
     * Generic delete of backed up KDE proxy settings
     *
     * @return boolean True if successful
     */
    private boolean deleteKDEProxyPrefs() {

        if (strBackupFolder == null) {
            return false;
        }

        boolean result = true;

        // Delete Kde prefs
        if (strKDEProxyPrefsBackup != null) {
            result = SimpleFile.delete(strKDEProxyPrefsBackup);
        }
        return result;
    }

    /**
     * Generic set proxy config
     *
     * @param url
     */
    public void setProxyAutoConfigURL(String url) {
        setGnomeProxyAutoConfigURL(url);
        setKDEProxyAutoConfigURL(url);
    }

    /**
     * Set Gnome proxy config
     *
     * @param url
     */
    private void setGnomeProxyAutoConfigURL(String url) {
        if (strGnome3ProxyPrefsBackup != null) {
            if (url == null) {
                OSFunction.resetGnome3Pref("org.gnome.system.proxy", "mode");
                OSFunction.resetGnome3Pref("org.gnome.system.proxy", "autoconfig-url");
            } else {
                OSFunction.setGnome3Pref("org.gnome.system.proxy", "mode", "auto");
                OSFunction.setGnome3Pref("org.gnome.system.proxy", "autoconfig-url", url);
            }
        }
    }

    /**
     * Set KDE proxy config
     *
     * @param url
     */
    private void setKDEProxyAutoConfigURL(String url) {

        if (strKDEProxyPrefsBackup != null) {
            if (url == null) {
                OSFunction.setKDEPref("Proxy Settings", "Proxy Config Script", "");
                OSFunction.setKDEPref("Proxy Settings", "ProxyType", 0);
            } else {
                OSFunction.setKDEPref("Proxy Settings", "Proxy Config Script", url);
                OSFunction.setKDEPref("Proxy Settings", "ProxyType", 2);
            }
        }
    }

    /**
     * Save pac file using given filename
     *
     * @param filename
     * @param ruleslist
     */
    private void savePACFile(String filename, ArrayList<String> ruleslist) {
        SimpleFile sfPacFile = new SimpleFile(strPACPath + filename);
        sfPacFile.openBufferedWrite();
        sfPacFile.writeFile("//" + filename, 1);
        String temp = "function FindProxyForURL(url, host) {";
        sfPacFile.writeFile(temp, 1);
        for (String s : ruleslist) {
            sfPacFile.writeFile(s, 1);
        }
        sfPacFile.writeFile("}", 1);
        sfPacFile.closeFile();
    }

    /**
     * Delete specified pac file
     *
     * @param filename
     * @return boolean true if action successful
     */
    public boolean deletePAC(String filename) {
        return SimpleFile.delete(strPACPath + filename);
    }

    /**
     * Delete all pac files
     */
    public void deleteAllPAC() {
        SimpleFile sf = new SimpleFile(strPACPath);
        sf.setFileFilter(PACFILE_EXT);
        sf.deleteFileList(sf.getFileList());
    }

    /**
     * Get a pseudo random numerical name
     *
     * @return String
     */
    private String createRandomName() {
        // Create new filename with a random number to modify filename from
        // previous one to force auto reload of pac settings in browser       
        int pacNumber = randgen.nextInt();
        if (pacNumber < 0) {
            pacNumber = 0 - pacNumber;
        }
        return String.valueOf(pacNumber);
    }

    /**
     * Set default proxy url
     *
     * @param proxyurl
     */
    public void setDefaultProxy(String proxyurl) {
        strDefaultProxy = proxyurl;
    }

    /**
     * Set do not proxy
     *
     * @param hostcsv
     */
    public void setDoNotProxy(String hostcsv) {
        strDoNotProxy = hostcsv;
    }

    /**
     * Get value of comma separated host or ip list
     *
     * @return String value
     */
    public String getDoNotProxy() {
        return strDoNotProxy;
    }

    /**
     * Adds pattern to an existing pattern file
     *
     * @param country
     * @param description
     * @param pattern
     */
    public void addToPatternFile(String country, String description, String pattern) {
        if (strPatternsFolder == null || pattern.isEmpty()) {
            return;
        }
        SimpleFile sfPatternFile = new SimpleFile(strPatternsFolder + SimpleFile.getSeparator() + country.substring(0, 2) + "_" + FILEUSER + FILEEXT);
        sfPatternFile.openBufferedAppend();
        sfPatternFile.writeFile("<" + description + "," + pattern + ",true>", 1);
        sfPatternFile.closeFile();
    }

    /**
     * Get an array of user pattern files
     *
     * @return array of files
     */
    public File[] getPatternsFiles() {
        SimpleFile sf = new SimpleFile(strPatternsFolder);
        sf.setFileFilter(FILEUSER + FILEEXT);
        return sf.getFileList();
    }

    /**
     * Export user patterns
     *
     * @param filename
     * @param files
     */
    public void exportUserPatterns(String filename, File[] files) {

        // Append .zip extension if we need to
        if (!filename.endsWith(".zip")) {
            filename += ".zip";
        }
        // Add user pattern files to zip
        SimpleFile sf = new SimpleFile(filename);
        sf.openZipStream(9);
        sf.addFilesToZip(files);
        sf.closeZipStream();
    }

    /**
     * Import patterns
     *
     * @param filename
     * @return true if succesful
     */
    public boolean importPatterns(String filename) {

        // Import patterns
        boolean success = false;
        SimpleFile sf = new SimpleFile(filename);
        String destpath;
        // Check existence
        if (sf.exists()) {
            // Get temporary path to extraction folder
            if (strTempFolder == null) {
                destpath = OSFunction.getTempFolder(null)
                        + getClass().getName();
            } else {
                destpath = strTempFolder + getClass().getName();
            }
            // Create temp extraction folder          
            SimpleFile.createFolder(destpath);
            // Extract files to temp folder
            success = sf.extractZipTo(destpath);
            if (success) {
                // Get list of files
                sf.setFileName(destpath);
                sf.setFileFilter(".txt");
                File[] files = sf.getFileList();
                // Verify each file before copying them over
                String firstLine;
                try {
                    if (files.length == 0) {
                        throw new Exception();
                    }
                    for (File f : files) {
                        // Verify valid pattern file by checking first line of file
                        sf.setFile(f);
                        sf.openBufferedRead();
                        firstLine = sf.readLine();
                        sf.closeFile();
                        if (firstLine != null) {
                            if (firstLine.toLowerCase().contains("pattern file")) {
                                SimpleFile.copyFromTo(f.getAbsolutePath(), strPatternsFolder + f.getName());
                            } else {
                                success = false;
                                break;
                            }
                        }
                    }
                } catch (Exception ex) {
                    success = false;
                }
            }
            // Delete temporary files
            SimpleFile.deleteFolder(destpath);
        }
        return success;
    }

}
