/*

   Licensed to the Apache Software Foundation (ASF) under one or more
   contributor license agreements.  See the NOTICE file distributed with
   this work for additional information regarding copyright ownership.
   The ASF licenses this file to You under the Apache License, Version 2.0
   (the "License"); you may not use this file except in compliance with
   the License.  You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.

 */
package org.apache.batik.util;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Point;
import java.awt.Rectangle;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.AccessControlException;
import java.util.ArrayList;
import java.util.Map;
import java.util.Properties;
import java.util.StringTokenizer;

/**
 * This class allows to manage users preferences.
 * <p>
 * Here is a short usage example:
 * <p>
 * <blockquote><pre>
 * // at application intialization
 * HashMap defaults = new HashMap();
 * defaults.put("windowSize", new Dimension(640, 480));
 * defaults.put("antialias", Boolean.TRUE);
 * PreferenceManager prefs = new PreferenceManager("application.ini", defaults);
 * try {
 *     prefs.load();
 * } catch (IOException e) {
 *     //
 * }
 * myApplication.setSize(prefs.getDimension("windowSize"));
 * myApplication.setAntialiasingOn(prefs.getBoolean("antialias"));
 *
 * // later a dialog box may customize preferences
 * myApplication.setAntialiasingOn(antialiasCheckBox.getState());
 * prefs.setBoolean("antialias", antialiasCheckBox.getState());
 *
 * // when leaving the application we need to save the preferences
 * prefs.setDimension("windowSize", myApplication.getSize());
 * prefs.setFiles("history", lastVisitedFileArray);
 * try {
 *    prefs.save()
 * } catch (IOException e) {
 *    //
 * }
 * </pre></blockquote>
 * <p>
 * @author <a href="mailto:cjolif@ilog.fr">Christophe Jolif</a>
 * @version $Id$
 */
public class PreferenceManager
{
    protected Properties internal = null;
    protected Map defaults = null;
    protected String prefFileName = null;
    protected String fullName = null;

    protected static final String USER_HOME = getSystemProperty("user.home");
    protected static final String USER_DIR  = getSystemProperty("user.dir");
    protected static final String FILE_SEP  = getSystemProperty("file.separator");

    private static String PREF_DIR = null;

    /**
     * Gets a System property if accessible. Returns an empty string
     * otherwise
     */
    protected static String getSystemProperty(String prop){
        try{
            return System.getProperty(prop);
        }catch(AccessControlException e){
            return "";
        }
    }

    /**
     * Creates a preference manager.
     * @param prefFileName the name of the preference file.
     */
    public PreferenceManager(String prefFileName)
    {
        this(prefFileName, null);
    }

    /**
     * Creates a preference manager with a default values
     * initialization map.
     * @param prefFileName the name of the preference file.
     * @param defaults where to get defaults value if the value is
     * not specified in the file.
     */
    public PreferenceManager(String prefFileName, Map defaults)
    {
        this.prefFileName = prefFileName;
        this.defaults = defaults;
        internal = new Properties();
    }

    /**
     * Sets a <code>String</code> representing the directory
     * where <code>PreferenceManager</code> instances should look
     * for preferences files. The default value is <code>null</code>
     * which means the automatic mechanism for looking for preferences
     * is used.
     * @see #load
     */
    public static void setPreferenceDirectory(String dir)
    {
        PREF_DIR = dir;
    }

    /**
     * Returns a <code>String</code> representing the directory
     * where <code>PreferenceManager</code> instances should look
     * for preferences.
     * @see #load
     * @see #setPreferenceDirectory
     */
    public static String getPreferenceDirectory()
    {
        return PREF_DIR;
    }

    /**
     * Loads the preference file. If the file has already been previously
     * sucessfuly loaded or saved, it will first try to reaload it from
     * this location. Otherwise, it will try to find the file
     * in the following order: in the directory set by
     * {@link #setPreferenceDirectory} if it exists, in the user
     * home directory and then in the current user directory.
     * @exception IOException if an error occured when reading the file.
     * @see #save
     */
    public void load()
        throws IOException
    {
        FileInputStream fis = null;
        if (fullName != null)
            try {
                fis = new FileInputStream(fullName);
            } catch (IOException e1) {
                fullName = null;
            }
        if (fullName == null) {
            if (PREF_DIR != null) {
                try {
                    fis =
                        new FileInputStream(fullName =
                                            PREF_DIR+FILE_SEP+prefFileName);
                } catch (IOException e2) {
                    fullName = null;
                }
            }
            if (fullName == null) {
                try {
                    fis =
                        new FileInputStream(fullName =
                                            USER_HOME+FILE_SEP+prefFileName);
                } catch (IOException e3) {
                    try {
                        fis = new FileInputStream(fullName =
                                                  USER_DIR+FILE_SEP+prefFileName);
                    } catch (IOException e4) {
                        fullName = null;
                    }
                }
            }
        }
        if (fullName != null) {
            try {
                internal.load(fis);
            } finally {
                fis.close();
            }
        }
    }

    /**
     * Saves the preference file. If it has previously sucessfuly been
     * loaded or save it will save it at the same location. In other cases
     * it will save it in the directory set by {@link #setPreferenceDirectory}
     * if has been set and exists, otherwise in the user home directory.
     * @exception IOException if an error occured when writing the file or
     * if is impossible to write the file at all available locations.
     * @see #load
     */
    public void save()
        throws IOException
    {
        FileOutputStream fos = null;
        if (fullName != null)
            try {
                fos = new FileOutputStream(fullName);
            } catch(IOException e1) {
                fullName = null;
            }
        if (fullName == null) {
            if (PREF_DIR != null) {
                try {
                    fos =
                        new FileOutputStream(fullName =
                                             PREF_DIR+FILE_SEP+prefFileName);
                } catch (IOException e2) {
                    fullName = null;
                }
            }
            if (fullName == null) {
                try {
                    fos =
                        new FileOutputStream(fullName =
                                             USER_HOME+FILE_SEP+prefFileName);
                } catch (IOException e3) {
                    fullName = null;
                    throw e3;
                }
            }
        }
        try {
            internal.store(fos, prefFileName);
        } finally {
            fos.close();
        }
    }

    private Object getDefault(String key)
    {
        if (defaults != null)
            return defaults.get(key);
        else
            return null;
    }

    /**
     * Returns a Rectangle preference.
     */
    public Rectangle getRectangle(String key)
    {
        Rectangle defaultValue = (Rectangle)getDefault(key);
        String sp = internal.getProperty(key);
        if (sp == null) {
            return defaultValue;
        }
        Rectangle result = new Rectangle();
        try {
            int x, y, w, h;
            String token;
            StringTokenizer st = new StringTokenizer(sp," ", false);
            if (!st.hasMoreTokens()) {
                // the value is not correctly formated => remove it
                internal.remove(key);
                return defaultValue;
            }
            token = st.nextToken();
            x = Integer.parseInt(token);
            if (!st.hasMoreTokens()) {
                internal.remove(key);
                return defaultValue;
            }
            token = st.nextToken();
            y = Integer.parseInt(token);
            if (!st.hasMoreTokens()) {
                internal.remove(key);
                return defaultValue;
            }
            token = st.nextToken();
            w = Integer.parseInt(token);
            if (!st.hasMoreTokens()) {
                internal.remove(key);
                return defaultValue;
            }
            token = st.nextToken();
            h = Integer.parseInt(token);
            result.setBounds(x,y,w,h);
            return result;
        } catch (NumberFormatException e) {
            internal.remove(key);
            return defaultValue;
        }
    }

    /**
     * Returns a Dimension preference.
     */
    public Dimension getDimension(String key)
    {
        Dimension defaultValue = (Dimension)getDefault(key);
        String sp = internal.getProperty(key);
        if (sp == null)
            return defaultValue;
        Dimension result = new Dimension();
        try {
            int w, h;
            String token;
            StringTokenizer st = new StringTokenizer(sp," ", false);
            if (!st.hasMoreTokens()) {
                // the value is not correctly formated => remove it
                internal.remove(key);
                return defaultValue;
            }
            token = st.nextToken();
            w = Integer.parseInt(token);
            if (!st.hasMoreTokens()) {
                internal.remove(key);
                return defaultValue;
            }
            token = st.nextToken();
            h = Integer.parseInt(token);
            result.setSize(w,h);
            return result;
        } catch (NumberFormatException e) {
            internal.remove(key);
            return defaultValue;
        }
    }

    /**
     * Returns a point preference.
     */
    public Point getPoint(String key)
    {
        Point defaultValue = (Point)getDefault(key);
        String sp = internal.getProperty(key);
        if (sp == null) {
            return defaultValue;
        }
        Point result = new Point();
        try {
            int x, y;
            String token;
            StringTokenizer st = new StringTokenizer(sp," ", false);
            if (!st.hasMoreTokens()) {
                // the value is not correctly formated => remove it
                internal.remove(key);
                return defaultValue;
            }
            token = st.nextToken();
            x = Integer.parseInt(token);
            if (!st.hasMoreTokens()) {
                internal.remove(key);
                return defaultValue;
            }
            token = st.nextToken();
            y = Integer.parseInt(token);
            if (!st.hasMoreTokens()) {
                internal.remove(key);
                return defaultValue;
            }
            result.setLocation(x,y);
            return result;
        } catch (NumberFormatException e) {
            internal.remove(key);
            return defaultValue;
        }
    }

    /**
     * Retruns a Color preference.
     */
    public Color getColor(String key)
    {
        Color defaultValue = (Color)getDefault(key);
        String sp = internal.getProperty(key);
        if (sp == null) {
            return defaultValue;
        }
        try {
            int r, g, b, a;
            String token;
            StringTokenizer st = new StringTokenizer(sp," ", false);
            if (!st.hasMoreTokens()) {
                // the value is not correctly formated => remove it
                internal.remove(key);
                return defaultValue;
            }
            token = st.nextToken();
            r = Integer.parseInt(token);
            if (!st.hasMoreTokens()) {
                internal.remove(key);
                return defaultValue;
            }
            token = st.nextToken();
            g = Integer.parseInt(token);
            if (!st.hasMoreTokens()) {
                internal.remove(key);
                return defaultValue;
            }
            token = st.nextToken();
            b = Integer.parseInt(token);
            if (!st.hasMoreTokens()) {
                internal.remove(key);
                return defaultValue;
            }
            token = st.nextToken();
            a = Integer.parseInt(token);
            return new Color(r, g, b, a);
        } catch (NumberFormatException e) {
            internal.remove(key);
            return defaultValue;
        }
    }

    /**
     * Returns a font preference.
     */
    public Font getFont(String key)
    {
        Font defaultValue = (Font)getDefault(key);
        String sp = internal.getProperty(key);
        if (sp == null) {
            return defaultValue;
        }
        try {
            int size, type;
            String name;
            String token;
            StringTokenizer st = new StringTokenizer(sp," ", false);
            if (!st.hasMoreTokens()) {
                // the value is not correctly formated => remove it
                internal.remove(key);
                return defaultValue;
            }
            name = st.nextToken();
            if (!st.hasMoreTokens()) {
                internal.remove(key);
                return defaultValue;
            }
            token = st.nextToken();
            size = Integer.parseInt(token);
            if (!st.hasMoreTokens()) {
                internal.remove(key);
                return defaultValue;
            }
            token = st.nextToken();
            type = Integer.parseInt(token);
            return new Font(name, type, size);
        } catch (NumberFormatException e) {
            internal.remove(key);
            return defaultValue;
        }

    }

    /**
     * Returns a String preference.
     */
    public String getString(String key)
    {
        String sp = internal.getProperty(key);
        if (sp == null) {
            sp = (String)getDefault(key);
        }
        return sp;
    }

    /**
     * Returns an array of String preference.
     */
    public String[] getStrings(String mkey)
    {
        String last;
        int i = 0;
        ArrayList v = new ArrayList();
        while (true) {
            last = getString(mkey+i);
            i++;
            if (last == null)
                break;
            v.add(last);
        }
        if (v.size() != 0) {
            String[] str = new String[v.size()];
            return (String[])v.toArray(str);
        } else {
            return (String[])getDefault(mkey);
        }
    }

    /**
     * Returns an URL preference.
     */
    public URL getURL(String key)
    {
        URL defaultValue = (URL)getDefault(key);
        String sp = internal.getProperty(key);
        if (sp == null) {
            return defaultValue;
        }
        URL url = null;
        try {
            url = new URL(sp);
        } catch (MalformedURLException ex) {
            internal.remove(key);
            return defaultValue;
        }
        return url;
    }

    /**
     * Returns an array of URLs preference.
     */
    public URL[] getURLs(String mkey)
    {
        URL last;
        int i = 0;
        ArrayList v = new ArrayList();
        while (true) {
            last = getURL(mkey+i);
            i++;
            if (last == null)
                break;
            v.add(last);
        }
        if (v.size() != 0) {
            URL[] path = new URL[v.size()];
            return (URL[])v.toArray(path);
        } else {
            return (URL[])getDefault(mkey);
        }
    }

    /**
     * Returns a File preference.
     */
    public File getFile(String key)
    {
        File defaultValue = (File)getDefault(key);
        String sp = internal.getProperty(key);
        if (sp == null) {
            return defaultValue;
        }
        File file = new File(sp);
        if (file.exists())
            return file;
        else {
            internal.remove(key);
            return defaultValue;
        }
    }

    /**
     * Returns an array of Files preference.
     */
    public File[] getFiles(String mkey)
    {
        File last;
        int i = 0;
        ArrayList v = new ArrayList();
        while (true) {
            last = getFile(mkey+i);
            i++;
            if (last == null)
                break;
            v.add(last);
        }
        if (v.size() != 0) {
            File[] path = new File[v.size()];
            return (File[])v.toArray(path);
        } else {
            return (File[])getDefault(mkey);
        }
    }


    /**
     * Gets an int preference.
     */
    public int getInteger(String key)
    {
        int defaultValue = 0;
        if (getDefault(key) != null)
            defaultValue = (Integer) getDefault(key);
        String sp = internal.getProperty(key);
        if (sp == null) {
            return defaultValue;
        }
        int value;
        try {
            value = Integer.parseInt(sp);
        } catch (NumberFormatException ex) {
            internal.remove(key);
            return defaultValue;
        }
        return value;
    }

    /**
     * Gets a float preference.
     */
    public float getFloat(String key)
    {
        float defaultValue = 0;
        if (getDefault(key) != null)
            defaultValue = (Float) getDefault(key);
        String sp = internal.getProperty(key);
        if (sp == null) {
            return defaultValue;
        }
        float value;
        try {
            value = Float.parseFloat(sp);
        } catch (NumberFormatException ex) {
            setFloat(key, defaultValue);
            return defaultValue;
        }
        return value;
    }

    /**
     * Gets a boolean preference. If not found and no default returns false.
     */
    public boolean getBoolean(String key)
    {
        if (internal.getProperty(key) != null)
            return internal.getProperty(key).equals("true");
        else
            if (getDefault(key) != null)
                return (Boolean) getDefault(key);
            else
                return false;
    }

    /**
     * Sets a Rectangle preference. If null removes it.
     */
    public void setRectangle(String key, Rectangle value)
    {
        if (value != null && !value.equals(getDefault(key)))
            internal.setProperty(key, value.x+" "+value.y+" "+
                                 value.width+ ' ' +value.height);
        else
            internal.remove(key);
    }

    /**
     * Sets a Dimension preference. If null removes it.
     */
    public void setDimension(String key, Dimension value)
    {
        if (value != null && !value.equals(getDefault(key)))
            internal.setProperty(key, value.width+" "+value.height);
        else
            internal.remove(key);
    }

    /**
     * Sets a Point preference. If null removes it.
     */
    public void setPoint(String key, Point value)
    {
        if (value != null && !value.equals(getDefault(key)))
            internal.setProperty(key, value.x+" "+value.y);
        else
            internal.remove(key);
    }

    /**
     * Sets a Color preference. If null removes it.
     */
    public void setColor(String key, Color value)
    {
        if (value != null && !value.equals(getDefault(key)))
            internal.setProperty(key, value.getRed()+" "+
                         value.getGreen()+" "+value.getBlue()+" "+
                         value.getAlpha());
        else
            internal.remove(key);
    }

    /**
     * Sets a Font preference. If null removes it.
     */
    public void setFont(String key, Font value)
    {
        if (value != null && !value.equals(getDefault(key)))
            internal.setProperty(key, value.getName()+" "+value.getSize()+" "+
                         value.getStyle());
        else
            internal.remove(key);
    }

    /**
     * Sets a String preference. If null removes it.
     */
    public void setString(String key, String value)
    {
        if (value != null && !value.equals(getDefault(key)))
            internal.setProperty(key, value);
        else
            internal.remove(key);
    }

    /**
     * Sets a String array preference. If null or size null removes
     * previous preference.
     */
    public void setStrings(String mkey, String[] values)
    {
        int j = 0;
        if (values != null)
            for (String value : values) {
                if (value != null) {
                    setString(mkey + j, value);
                    j++;
                }
            }
        // erase other elements
        String last;
        while (true) {
            last = getString(mkey+j);
            if (last == null)
                break;
            setString(mkey+j, null);
            j++;
        }
    }

    /**
     * Sets an URL property. If null removes it.
     */
    public void setURL(String key, URL value)
    {
        if (value != null && !value.equals(getDefault(key)))
            internal.setProperty(key, value.toString());
        else
            internal.remove(key);
    }

    /**
     * Sets an array of URLs property. If null or size null removes
     * previous preference.
     */
    public void setURLs(String mkey, URL[] values)
    {
        int j = 0;
        if (values != null)
            for (URL value : values) {
                if (value != null) {
                    setURL(mkey + j, value);
                    j++;
                }
            }
        // erase other elements
        String last;
        while (true) {
            last = getString(mkey+j);
            if (last == null)
                break;
            setString(mkey+j, null);
            j++;
        }
    }

    /**
     * Sets a File property. If null removes it.
     */
    public void setFile(String key, File value)
    {
        if (value != null && !value.equals(getDefault(key)))
            internal.setProperty(key, value.getAbsolutePath());
        else
            internal.remove(key);
    }

    /**
     * Sets an array of Files property. If null or size null removes
     * previous preference.
     */
    public void setFiles(String mkey, File[] values)
    {
        int j = 0;
        if (values != null)
            for (File value : values) {
                if (value != null) {
                    setFile(mkey + j, value);
                    j++;
                }
            }
        // erase other elements
        String last;
        while (true) {
            last = getString(mkey+j);
            if (last == null)
                break;
            setString(mkey+j, null);
            j++;
        }
    }

    /**
     * Sets an int property.
     */
    public void setInteger(String key, int value)
    {
        if (getDefault(key) != null &&
                (Integer) getDefault(key) != value)
            internal.setProperty(key, Integer.toString(value));
        else
            internal.remove(key);
    }

    /**
     * Sets a float property.
     */
    public void setFloat(String key, float value)
    {
        if (getDefault(key) != null &&
                (Float) getDefault(key) != value)
            internal.setProperty(key, Float.toString(value));
        else
            internal.remove(key);
    }

    /**
     * Sets a boolean property.
     */
    public void setBoolean(String key, boolean value)
    {
        if (getDefault(key) != null &&
                (Boolean) getDefault(key) != value) {
            internal.setProperty(key, value?"true":"false");
        } else {
            internal.remove(key);
        }
    }
}
