package misc;

import java.awt.Component;
import java.awt.Point;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URL;
import java.text.DecimalFormat;
import java.text.MessageFormat;
import java.util.Date;
import java.util.Locale;
import java.util.Properties;
import java.util.Vector;
import javax.swing.JComboBox;
import javax.swing.JList;
import javax.swing.JOptionPane;
import javax.swing.ListModel;

/**
   Managed the persistance key/value strings association.
   This class could be used to managed all constants values in a application.
   The values commme from a bundle jar (mix of class and data) or a dedicated directeory.
*/
public class Config {

    static public final String FS = System.getProperty ("file.separator");
    
    static public final String
	dataDirname	= "data",
	configDirname	= "config",
	textsDirname	= "texts",
	logDirname	= "log",
	imagesDirname	= "images",
	iconsDirname	= "images",
	buttonDirname	= "button",
	flagsDirname	= "flags";

    /** Path to configuration files in files system. */
    static public final String
	configSystemDir	= dataDirname+FS+configDirname+FS,
	logSystemDir	= dataDirname+FS+logDirname+FS,
	buttonSystemDir	= dataDirname+FS+imagesDirname+FS+buttonDirname+FS;

    /** Path to configuration files in jar. */
    static public final String
	configJarDir	= dataDirname+"/"+configDirname+"/",
	imagesJarDir	= dataDirname+"/"+imagesDirname+"/",
	textsJarDir	= dataDirname+"/"+textsDirname+"/";

    /** Configuration file extension. */
    static public final String
	configExt	= ".xml",
	iconsExt	= ".png",
	imagesExt	= ".png",
	htmlExt		= ".html",
	logExt		= ".log";


    /** Enconding charset for saving configuration. */
    static public final String configEncoding = "UTF8";

    /** Comment used as header configuration file. */
    static public final String configHeaderFile =
	"This file is automaticaly generated by {0} application at {1,time,short} on {1,date}.";

    /** Somme postfix for constants name in configuration file. */
    static public final String
	checkedPostfix = "Checked",
	undockedPostfix = "Undocked",
	locationPostfix = "Location";

    /** Message shows when trying to get string with a null key. */
    static private final String configNullKeyException = "No configuration associates with null key.";

    /** Message shows when trying to use configuration before load it. */
    static private final String configUseException = "No configuration loaded.";

    /** Message shows when load exception appears. */
    static private final String configLoadException = "Configuration {0} can''t be load (corrupted file ?).";

    /** Message shows when save exception appears. */
    static private final String configSaveException = "Configuration {0} can''t be save.";

    /** Liste of constants (key/value strings association). */
    //static private Properties configuration;
    static public Properties configuration;

    /** True if configuration need to be saved. */
    static private boolean configurationModified = false;

    // ========================================
    static private File PWD;
    static private String[] dataPath;
    static {
	setPWD (Util.class);
	setDataPath (".:..");
    }

    static public File getJarFileOrClassDir (Class<?> applicationClass) {
	try {
	    return new File (applicationClass.getProtectionDomain ().getCodeSource ().getLocation ().toURI ());
	} catch (Exception e) {
	    // dosn't work for applet
	    return null;
	}
    }
    static public File getPWD () { return PWD; }
    // XXX static public String getPWD () { return PWD.getAbsolutePath (); }
    static public void setPWD (Class<?> applicationClass) {
	try {
	    PWD = getJarFileOrClassDir (applicationClass).getParentFile ();
	} catch (Exception e) {
	}
    }
    static public void setDataPath (String path) {
	dataPath = path.split (":");
    }
    static public File findDataDir () {
	return findDataDir (false);
    }
    static public File findDataDir (boolean writable) {
	String name = dataDirname;
	try {
	    // search from excecution dir
	    File file = (new File (name));
	    if (file.exists () && file.isDirectory () && (!writable || file.canWrite ()))
		return file;
	} catch (Exception e) {
	}
	for (String path : dataPath) {
	    try {
		// search in relative path from jar
		File file = (new File (PWD, path+FS+name));
		if (file.exists () && file.isDirectory () && (!writable || file.canWrite ()))
		    return file;
	    } catch (Exception e) {
	    }
	}
	return null;
    }

    static public URL getDataUrl (String... names) {
	if (names.length < 1)
	    return null;
	String name = names[names.length - 1];
	String jarPath = "", systemPath = "";
	for (int i = 0; i < names.length - 1; i++) {
	    jarPath += names[i]+"/";
	    systemPath += names[i]+FS;
	}
	try {
	    // search from excecution dir
	    File file = new File (systemPath+name);
	    if (file.exists ())
		return file.toURI ().toURL ();
	} catch (Exception e) {
	}
	for (String path : dataPath) {
	    try {
		// search in relative path from jar
		File file = new File (PWD, path+FS+systemPath+name);
		if (file.exists ())
		    return file.getCanonicalFile ().toURI ().toURL ();
	    } catch (Exception e) {
	    }
	}
	try {
	    // search in jar
	    URL result = ClassLoader.getSystemResource (jarPath+name);
	    return result;
	} catch (Exception e) {
	}
	return null;
    }

    // ========================================
    /**
       Load XML configuration file contains constants (key/value strings association).
       @param applicationName the application name use to find file.
    */
    static public final void load (String applicationName) {
	configuration = new Properties ();
	try {
	    File configDir = new File (findDataDir (), configDirname);
	    File configFile = new File (configDir, applicationName+configExt);
	    FileInputStream fileInputStream = new FileInputStream (configFile);
	    configuration.loadFromXML (fileInputStream);
	    fileInputStream.close ();
	    configurationModified = false;
	} catch (Exception e) {
	    try {
		configuration.loadFromXML (ClassLoader.getSystemResourceAsStream
					   (configJarDir+applicationName+configExt));
		configurationModified = false;
	    } catch (Exception e2) {
		try {
		    configuration.loadFromXML (ClassLoader.getSystemResourceAsStream
					       (configJarDir+applicationName+configExt));
		    configurationModified = false;
		} catch (Exception e3) {
		    if (JOptionPane.YES_OPTION != JOptionPane.showConfirmDialog
			(null, "Would you like to continue?",
			 " Can't load Configuration ", JOptionPane.YES_NO_OPTION))
			throw new IllegalArgumentException (MessageFormat.format (configLoadException, applicationName));
		}
	    }
	}
    }

    // ========================================
    /**
       Return the loading state of the configuration.
       @return true if the configuration is loaded.
    */
    static public boolean isLoaded () {
	return configuration != null;
    }

    // ========================================
    /**
       Save XML configuration file contains key/value strings association.
       The file is not writing if no modification occure.
       @param applicationName the application name use to save file.
    */
    static public final void save (String applicationName) {
	File configDir = new File (findDataDir (true), configDirname);
	File configFile = new File (configDir, applicationName+configExt);
	try {
	    save (applicationName, configFile);
	} catch (IOException e) {
	    try {
		e.printStackTrace ();
		configDir.mkdirs ();
		save (applicationName,configFile);
	    } catch (IOException e2) {
		System.err.println (MessageFormat.format (configSaveException, applicationName));
	    }
	} catch (NullPointerException e) {
	    throw new IllegalArgumentException (configUseException);
	}
    }

    static public final void save (String applicationName, File file)
	throws IOException {
	if (!configurationModified && file.exists ())
	    return;
	if (!file.exists ())
	    file.createNewFile ();
	FileOutputStream fileOutputStream = new FileOutputStream (file);
	configuration.storeToXML (fileOutputStream,
				  (new MessageFormat (configHeaderFile, Locale.US)).format (new Object[] {applicationName, new Date ()},
											    new StringBuffer(), null).toString (),
				  configEncoding);
	fileOutputStream.close ();
	configurationModified = false;
    }

    // ========================================

    // XXX defaultValue peut être placé systématiquement (voir à null)
    /**
       Searches for the value with the specified key in this configuration list.
       @param key the name of the configuration parameter.
       @return the configuration value associated with the key or null if not found.
    */
    static public final String getString (String key) {
	if (key == null)
	    throw new IllegalArgumentException (configNullKeyException);
	try {
	    return configuration.getProperty (key);
	} catch (NullPointerException e) {
	    throw new IllegalArgumentException (configUseException);
	}
    }

    /**
       Searches for the value with the specified key in this configuration list.
       @param key the name of the configuration parameter.
       @param defaultValue the defaultValue in case the key is not found.
       @return the configuration value associated with the key or defaultValue if not found.
    */
    static public final String getString (String key, String defaultValue) {
	String value = configuration.getProperty (key);
	if (value != null)
	    return value;
	configuration.setProperty (key, defaultValue);
	return defaultValue;
    }

    /**
       Change the value with the specified key in this configuration list.
       @param key the name of the configuration parameter.
       @param value the new configuration value associated with the key.
    */
    static public final void setString (String key, String value) {
	try {
	    configuration.setProperty (key, value);
	    configurationModified = true;
	} catch (NullPointerException e) {
	    throw new IllegalArgumentException (configUseException);
	}
    }

    // ========================================
    static public final File getFile (String key) {
	String fileName = getString (key);
	if (fileName == null)
	    return null;
	return new File (fileName);
    }

    static public final void setFile (String key, File file) {
	setString (key, file.getPath ());
    }

    // ========================================
    static public final boolean getBoolean (String key) {
	return Boolean.parseBoolean (getString (key));
    }

    static public final boolean getBoolean (String key, boolean defaultValue) {
	return Boolean.parseBoolean (getString (key, ""+defaultValue));
    }

    static public final void setBoolean (String key, boolean value) {
	setString (key, ""+value);
    }

    // ========================================
    static public final int getInt (String key) {
	return Integer.parseInt (getString (key));
    }

    static public final int getInt (String key, int defaultValue) {
	try {
	    return Integer.parseInt (getString (key, ""+defaultValue));
	} catch (Exception e) {
	    return defaultValue;
	}
    }

    static public final void setInt (String key, int value) {
	setString (key, ""+value);
    }

    // ========================================
    static public final float getFloat (String key) {
	return Float.parseFloat (getString (key));
    }

    static public final float getFloat (String key, float defaultValue) {
	try {
	    return Float.parseFloat (getString (key, ""+defaultValue));
	} catch (Exception e) {
	    return defaultValue;
	}
    }

    static public final void setFloat (String key, float value) {
	setString (key, ""+value);
    }

    // ========================================
    static public final double getDouble (String key) {
	return Double.parseDouble (getString (key));
    }

    static public final double getDouble (String key, double defaultValue) {
	try {
	    return Double.parseDouble (getString (key, ""+defaultValue));
	} catch (Exception e) {
	    return defaultValue;
	}
    }

    static public final void setDouble (String key, double value) {
	setString (key, ""+value);
    }

    // ========================================
    static public final<T> T getEnum (String key, T defaultValue) {
	return Util.toEnum (getString (key, ""+defaultValue), defaultValue);
    }

    // ========================================
    static public final DecimalFormat indexFormat = new DecimalFormat ("-0000");

    static public final Vector<String> getList (String key, String defaultValue) {
	Vector<String> result = new Vector<String> ();
	int size = Integer.parseInt (getString (key+"-size", "1"));
	result.add (getString (key, defaultValue));
	for (int i = 1; i < size; i++)
	    result.add (getString (key+indexFormat.format (i)));
	return result;
    }

    static public final void setList (String key, Vector<String> value) {
	// XXX suppression des anciennes valeurs
	int size = value.size ();
	int max = Integer.parseInt (getString (key+"-max", "10"));
	setString (key+"-max", ""+max);
	setString (key+"-size", ""+size);
	setString (key, value.elementAt (0));
	size = Math.min (size, max);
	for (int i = 1; i < size; i++)
	    setString (key+indexFormat.format (i), value.elementAt (i));
    }

    // ========================================
    // XXX faire un defaultValue en vector
    static public final void loadJList (String key, JList<String> jList, String defaultValue) {
	Vector<String> list = getList (key, defaultValue);
	jList.setListData (list);
	jList.setSelectedIndex (0);
    }

    static public final void saveJList (String key, JList<String> jList) {
	Vector<String> result = new Vector<String> ();
	ListModel<String> model = jList.getModel ();
	int size = model.getSize ();
	for (int i = 0; i < size; i++)
	    result.add (model.getElementAt (i));
	int index = jList.getSelectedIndex ();
	if (index >= 0) {
	    result.insertElementAt (result.remove (index), 0);
	    jList.setListData (result);
	    jList.setSelectedIndex (0);
	}
	setList (key, result);
    }

    // ========================================
    // XXX faire un defaultValue en vector
    static public final void loadJComboBox (String key, JComboBox<String> jComboBox, String defaultValue) {
	Vector<String> list = getList (key, defaultValue);
	jComboBox.removeAllItems ();
	for (int i = 0; i < list.size (); i++)
	    jComboBox.addItem (list.elementAt (i));
	jComboBox.setSelectedIndex (0);
    }

    static public final void loadJComboBoxInteger (String key, JComboBox<Integer> jComboBox, String defaultValue) {
	Vector<String> list = getList (key, defaultValue);
	jComboBox.removeAllItems ();
	for (int i = 0; i < list.size (); i++) {
	    try {
		jComboBox.addItem (Integer.parseInt (list.elementAt (i)));
	    } catch (Exception e) {
	    }
	}
	jComboBox.setSelectedIndex (0);
    }

    static public final void saveJComboBox (String key, JComboBox<String> jComboBox) {
	// XX peut être ajouter la valeur éditer dans la liste
	Vector<String> result = new Vector<String> ();
	int size = jComboBox.getItemCount ();
	for (int i = 0; i < size; i++)
	    result.add (jComboBox.getItemAt (i));
	int index = jComboBox.getSelectedIndex ();
	if (index < 0)
	    result.insertElementAt ((String) jComboBox.getSelectedItem (), 0);
	else
	    result.insertElementAt (result.remove (index), 0);
	if (index != 0) {
	    jComboBox.removeAllItems ();
	    for (int i = 0; i < result.size (); i++)
		jComboBox.addItem (result.elementAt (i));
	    jComboBox.setSelectedIndex (0);
	}
	setList (key, result);
    }

    static public final void saveJComboBoxInteger (String key, JComboBox<Integer> jComboBox) {
	// XX peut être ajouter la valeur éditer dans la liste
	Vector<String> result = new Vector<String> ();
	int size = jComboBox.getItemCount ();
	for (int i = 0; i < size; i++)
	    result.add (""+jComboBox.getItemAt (i));
	int index = jComboBox.getSelectedIndex ();
	if (index < 0)
	    result.insertElementAt ((String) jComboBox.getSelectedItem (), 0);
	else
	    result.insertElementAt (result.remove (index), 0);
	setList (key, result);
    }

    // ========================================
    /** Text format used to represent coordonates (i.e. [x=123,y=456] ). */
    static public final MessageFormat coordonateFormat = new MessageFormat ("[x={0,number,integer},y={1,number,integer}]");

    /**
       Set component location from a constant coordonates in configuration.
       @param key the name used to denoted the contant.
       @param component modified by the coordonates retreived in configuration.
       @param defaultLocation default coordonates used if non key present.
    */
    static public final void loadLocation (String key, Component component, Point defaultLocation) {
	try {
	    Object [] location = coordonateFormat.parse (getString (key+locationPostfix),
							 new java.text.ParsePosition (0));
	    component.setLocation (((Number) location [0]).intValue (), ((Number) location [1]).intValue ());
	} catch (Exception e) {
	    component.setLocation (defaultLocation);
	}
    }

    /**
       Save constant coordonates to configuration form a component location.
       @param key the name used to denoted the contant.
       @param component used to set the contant coordonates value.
    */
    static public final void saveLocation (String key, Component component) {
	Point location = component.getLocation ();
	setString (key+locationPostfix,
		   coordonateFormat.format (new Object [] {location.x, location.y},
					    new StringBuffer(), null).toString ());
    }

    // ========================================
}
