package com.superliminal.uiutil;

import javax.swing.*;
import java.util.*;
import java.util.prefs.Preferences;
import java.io.*;
import java.awt.*;
import java.net.*;

/**
 * Title:        PropertyManager
 * Description:  Methods for getting property strings from cascading Properties objects.
 *               The order of precidence is as follows:
 *               <code>
 *                   top
 *                   system
 *                   userprefs
 *                   vendorprops
 *                   defaults
 *               </code>
 *
 *               The root of the chain is stored in the public static "top" member which should be
 *               accessed for nearly all application purposes. Applications can set values there
 *               as well which will take precedence over lower-level defaults but will not be
 *               persisted across sessions.
 *
 *               Property file names are assumed to be rooted in a folder named "resources"
 *               expected to be found in the classpath. In that folder is expected to be
 *               a property file named "defaults.prop". That file should contain default values
 *               for all properties the application might request. This becomes the lowest level
 *               Properties in a chain.
 *
 *               An optional vendor-supplied property file may also be provided in which
 *               vendors may specify overrides for custom versions of a published sub-set of
 *               the default properties such as branding logos, colors, etc. A vendor property
 *               file must be specified as a URL in an environment variable with the key
 *               "vendorprops". e.g.  vendorprops=http://newcorp.com/analyer/resources/vendor.prop.
 *               the values specified in this file will take precidence over the analyzer defaults.
 *               Note that images and other file resources referred to by that file must be
 *               begin with the '/' character and will then be looked for relative to the
 *               directory containing the vendor property file.
 *               e.g. given the vendor prop file path above and the property setting:
 *                     main.logo.small=/newcorp.gif
 *               will resolve to:
 *                     newcorp.com/analyer/resources/newcorp.gif
 *               Also, quoted values will have their quotes stripped. This is in case a user
 *               wants to create a path or other property that begins with '/'.
 *
 *               Another special properties object in the chain is "userprefs" which is for end
 *               user preferences which are stored in a file on the user's local machine in their home directory.
 *               Properties that are set into this object are immediately persisted to the file
 *               for retrieval in the current and future sessions.
 *
 * Copyright 2005, 2006 - Superliminal Software
 * @author Melinda Green
 */
public class PropertyManager extends Properties {
    private final static String PRODUCT_NAME = "App"; // wants to be passed in but would need to be environment variable.
    private final static String PREFS_ROOT_NAME = "com.superliminal.userprefs";

    /** apps should load any user-specific property overrides directly into this object and call setProperty to customize. */
    public static PropertyManager userprefs = new LocalProps(new File(getUserPrefFile()));
    private final static Properties sysprops = new PropertyManager(userprefs);

    /**
     * apps should typically only call getProperty on this object
     * although it is ok to store program arguments and other session overrides here too.
     */
    public final static PropertyManager top = new PropertyManager(sysprops);

    static { init(); } // to perform static initialization

    /** users have no business subclassing this class so a private empty constructor will forbid it. */
    private PropertyManager() {}

    public PropertyManager(Properties defaults) { super(defaults); }

    /**
     * Utility to load a properties object from a file.
     * @param prop_url points to a property file
     * @param into is the Properties object to populate.
     */
    public static void loadProps(URL prop_url, Properties into) {
        if(prop_url == null) {
            //System.err.println("PropertyManager.loadProps: passed null url");
            return;
        }
        try {
            URLConnection connection = prop_url.openConnection();
            InputStream propstream = connection.getInputStream();
            into.load(propstream);
        } catch (Exception e) {
            System.err.println("PropertyManager.init: Couldn't load property file '" + prop_url.getPath() +
                "'. Make sure this subpath can be found within the classpath.");
        }
    }

    /**
     * Utility to load a properties object from a file.
     * @param prop_file_name names a file expected to be found under ./resources
     * @param into is the Properties object to populate.
     */
    public static void loadProps(String prop_file_name, Properties into) {
        String path = "resources" + File.separatorChar + prop_file_name;
        URL propurl = PropertyManager.class.getClassLoader().getResource(path);
        loadProps(propurl, into);
    }

    /**
     * Utility to load a set of command line arguments into a Properties object.
     * All argument names are expected to begin with a minus. If followed by an argument
     * without a minus, that argument is taken as the value for the one with the minus.
     * Arguments followed directly by another flagged argument are taken as boolean arguments
     * whos values are set to the string "true".
     *
     * @param args typically an args array from a main method
     * but with extracted elements possibly nulled out.
     * @param into the Properties file to load into.
     */
    public static void loadProps(String args[], Properties into) {
        for (int i = 0; i < args.length; i++) {
            if(args[i] == null)
                continue; // skip any nulled out elements (caller propably used and extracted them)
            if (args[i].startsWith("-")) {
                // Make sure there's another arg
                if ((i + 1) < args.length) {
                    // Make sure it's not another flag
                    if(args[i+1] == null || args[i + 1].startsWith("-")) {
                        // Must be a flag without a value, set to "true"
                        into.setProperty(args[i].substring(1), "true");
                    }
                    else {
                        into.setProperty(args[i].substring(1), args[i + 1]);
                        ++i; // skip to next arg pair
                    }
                } else {
                    // Must be a flag without a value at the end of the args
                    into.setProperty(args[i].substring(1), "true");
                }
            } else {
                // argument without a dash; must be malformed
                System.err.println("Invalid propertyfile argument: " + args[i]);
            }
        }
    }


    /**
     * A specialized PropertyManager that loads from a given local file.
     * Properties that are set on these objects are immediately persisted to that file
     * regardless of whether it existed originally.
     * This class is meant for handling user preferences that persist across sessions.
     */
    private static class LocalProps extends PropertyManager {
        private File localPropFile;
        private boolean storeFailed = false;
        public LocalProps(File localPropFile) {
            this.localPropFile = localPropFile;
            if( ! localPropFile.exists())
                return; // nothing to load
            try {
                load(new FileInputStream(localPropFile));
            } catch (IOException e) {
                System.err.println("PropertyManager.LocalProps: Could not load local prop file '" + localPropFile.getAbsolutePath() + "'");
                this.localPropFile = null;
            }
        }
        public void setFile(File newFile) {
            localPropFile.renameTo(newFile);
            localPropFile = newFile;
            storeFailed = false;
            writeToFile();
        }
        /**
         * Calls super.setProperty() and then immediately attempts to store the entire contents to the user's preference file.
         */
        public Object setProperty(String key, String value) {
            Object ret = super.setProperty(key, value);
            writeToFile();
            return ret;
        }
        /**
         * Calls super.clear() and then immediately attempts to empty the user's preference file.
         */
        public void clear() {
            super.clear();
            writeToFile();
        }
        /**
         * Calls super.remove() and then immediately attempts to store the entire contents to the user's preference file.
         */
        public Object remove(Object key) {
            Object ret = super.remove(key);
            writeToFile();
            return ret;
        }
        /**
         * Attempts to store the entire contents to the user's preference file.
         */
        private void writeToFile() {
            if(localPropFile==null || storeFailed)
                return;
            try {
                FileOutputStream fos = new FileOutputStream(localPropFile);
                this.store(fos, PRODUCT_NAME + " User Preferences -- The location of this file can be changed via Edit->Preferences");
                fos.close(); // important for renames to work
            } catch (IOException e) {
                storeFailed = true; // so as to only give fail msg once
                System.err.println("PropertyManager.LocalProps: Could not store local prop file '" + localPropFile.getAbsolutePath() + "'");
                e.printStackTrace(); // crude exception handling but we can't count on logging or other services available.
            }
        }
        /**
         * Overrides to return keys alphabeticaly so that the store method will also write them in that order.
         * NOTE: The output of this method will always be sorted but for the sorted store operation to work
         * depends upon the Java implementation continuing to call this method when iterating.
         * That is likely but could possibly change.
         * @return alphabetically sorted key set.
         */
        public Enumeration<Object> keys() {
            Vector keyList = new Vector();
            for(Enumeration keysEnum = super.keys(); keysEnum.hasMoreElements(); )
                keyList.add(keysEnum.nextElement());
            Collections.sort(keyList);
            return keyList.elements();
        }
    } // end class LocalProps


    /**
     * @return the path to where the user's preference file is or will be living.
     */
    public static String getUserPrefFile() {
        //String userdefault = new File(javax.swing.filechooser.FileSystemView.getFileSystemView().getHomeDirectory(), PRODUCT_NAME+".props").getAbsolutePath();
        File redsealdir = new File(System.getProperty("user.home") + File.separator + "redseal");
        if( ! redsealdir.exists())
            redsealdir.mkdir();
        String userdefault = new File(redsealdir, PRODUCT_NAME+".props").getAbsolutePath();
        return Preferences.userRoot().node(PREFS_ROOT_NAME).get(PRODUCT_NAME, userdefault);
    }

    /**
     * Sets the path for where user preferences will live.
     * @param newLoc is a file path to where the user preferences should now live.
     */
    public static void setUserPrefFile(String newLoc) {
        ((LocalProps)userprefs).setFile(new File(newLoc));
        Preferences.userRoot().node(PREFS_ROOT_NAME).put(PRODUCT_NAME, newLoc);
    }

    /**
     * High-level method that presents a file save dialog allowing the user to select a new location
     * for their user preferences file.
     * @param parent parent for the presented file chooser.
     */
    public static void showPrefFileChooser(Component parent) {
        JFileChooser chooser = new JFileChooser(new File(getUserPrefFile()));
        //System.out.println("current file loc: " + getUserPrefFile());
        if(chooser.showSaveDialog(parent) == JFileChooser.APPROVE_OPTION) {
            String newloc = chooser.getSelectedFile().getAbsolutePath();
            //System.out.println("new location: " + newloc);
            setUserPrefFile(newloc);
        }
    }


    /**
     * A specialized PropertyManager that attempts to load a set of properties from a URL
     * representing a set of VAR, reseller, or customer-specific overrides for an
     * application's default property values.
     * See the ClientProp documentation above for descriptions of the subtle syntax
     * differences for file locations and quoted keys.
     */
    private static class RemoteProps extends Properties {
        private String prefix;
        public RemoteProps(String fileurl, Properties def) {
            defaults = def;
            if(fileurl == null)
                return;
            URL url = null;
            try { url = new URL(fileurl); }
            catch(MalformedURLException e) {
                System.err.println("Couldn't open remote property file: " + fileurl);
            }
            if(url != null)
                loadProps(url, this);
            prefix = fileurl.substring(0, fileurl.lastIndexOf('/'));
        }
        /**
         * @return normal getProperty value except that for values beginning with '/',
         * prepends url path refering to same directory as the remote property file
         * given to the constructor. Also, quoted values will have their quotes striipped.
         * This is in case a user wants to create a path or other property beginning with '/'.
         */
        public String getProperty(String key) {
            // for debugging vendor props, if uncommented, this will provide a good line
            // on which to set a breakpoint:
            //if(key.equalsIgnoreCase("main.logo.small"))
            //    key = key;
            String val = (String) get(key);
            if(val == null)
                return defaults.getProperty(key);
            if(val.startsWith("/")) // path is relative to vendor prop file location
                val = prefix + val;
            if(val.startsWith("\"") && val.endsWith("\"")) // quoted strings for root paths or to contain spaces
                val = val.substring(1, val.length()-1);
            return val;
        }
        public String getProperty(String key, String dflt) {
            String val = getProperty(key);
            return val == null ? dflt : val;
        }
    } // end class RemoteProps


    /**
     * Static initializer to bootstrap the system.
     */
    private static void init() {
        Properties sys = System.getProperties();
        for(Enumeration e=sys.keys(); e.hasMoreElements(); ) {
            String key = (String)e.nextElement();
            sysprops.setProperty(key, sys.getProperty(key));
            //System.out.println(key + " = " + sys.getProperty(key));
        }
        PropertyManager defs = new PropertyManager();
        loadProps("defaults.prop", defs);
        // load any vendor-specific property file specified.
        // note, for testing, you can set a vm environment variable via something like:
        // -Dvendorprops=file:///C|/Superliminal/MC4D/resources/vendor.prop
        // or just hardcode with a line like this:
        // vendorpropfile = "file:///C|/Superliminal/MC4D/resources/vendor.prop";
        String vendorpropfile = System.getProperty("vendorprops");
        Properties vendorprops = new RemoteProps(vendorpropfile, defs);
        userprefs.defaults = new PropertyManager(vendorprops);
    }

    /**
     * Helper function to retrive integer values from the top properties.
     * @param key property name
     * @param def default value
     * @return integer value of top.getProperty(key) or default if not found or parsed.
     */
    public static int getInt(String key, int def) {
        try { return Integer.parseInt(top.getProperty(key)); }
        catch(NumberFormatException nfe) {}
        return def;
    }
    
    /**
     * Helper function to retrive float values from the top properties.
     * @param key property name
     * @param def default value
     * @return float value of top.getProperty(key) or default if not found or parsed.
     */
    public static float getFloat(String key, float def) {
        try { return Float.parseFloat(top.getProperty(key)); }
        catch(Exception e) {}
        return def;
    }
    
    /**
     * Helper function to retrive boolean values from the top properties.
     * @param key property name
     * @param def default value
     * @return boolean value of top.getProperty(key) or default if not found or parsed.
     */
    public static boolean getBoolean(String key, boolean def) {
        try { 
            String topval = top.getProperty(key);
            if(topval == null)
                return def;
            return Boolean.parseBoolean(topval);
        }
        catch(Exception e) {}
        return def;
    }      

    /**
     * Helper function to retrive color objects from properties with the format
     * r,g,b where each channel is a value from 0 to 255.  The color can also
     * be in hexadecimal format if it begins with "#", e.g., "#aabbcc"
     * @param key the color property name
     * @return a Color object with the parsed red, green, and blue values.
     */
    public static Color getColor(String key, Color def) {
        String str = top.getProperty(key);
        if (str == null)
            return def;
        if (str.indexOf('#') >= 0)
            return Color.decode(str);

        StringTokenizer toc = new StringTokenizer(str, ",");
        int r = Integer.parseInt(toc.nextToken());
        int g = Integer.parseInt(toc.nextToken());
        int b = Integer.parseInt(toc.nextToken());
        return new Color(r, g, b);
    }
    public static Color getColor(String key) {
        return getColor(key, null);
    }

    /**
     * Utility to construct a simple http url from keys to host and port properties.
     * @return an http url string.
     */
    public static String getURL(String hostkey, String portkey) {
        return "http://" + top.getProperty(hostkey) + ":" + top.getProperty(portkey) + "/";
    }

    /**
     * Simple example program.
     */
    public static void main(String args[]) {
        // test of setting a top-level property, e.g. a setting specified on command line.
        PropertyManager.top.setProperty("debugging", "true");
        // get a bunch of application values from the property files
        System.out.println("main.background = " + getColor("main.background"));
        System.out.println("tables.header.background = " + top.getProperty("tables.header.background") + " -> "  +  getColor("tables.header.background"));
        System.out.println("title = " + PropertyManager.top.getProperty("main.title"));
        // get a system property
        System.out.println("os.name = " + PropertyManager.top.getProperty("os.name"));
        // get a true top level (i.e. command line) property
        System.out.println("debugging = " + PropertyManager.top.getProperty("debugging"));
        int runcount = PropertyManager.getInt("test.runcount", 0);
        System.out.println("test.runcount = " + runcount);
        PropertyManager.userprefs.setProperty("test.runcount", ++runcount+"");
        System.out.println("test.runcount now = " + PropertyManager.userprefs.getProperty("test.runcount"));
        PropertyManager.userprefs.clear(); // testing that clearing userprefs writes the file
        System.out.println("num pref keys after clear: " + PropertyManager.userprefs.size());
        PropertyManager.userprefs.setProperty("test.runcount", runcount+"");
        showPrefFileChooser(null);
    }
}