package usda.weru.util;

import de.schlichtherle.io.File;
import de.schlichtherle.io.FileInputStream;
import de.schlichtherle.io.FileWriter;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.IOException;
import java.io.UTFDataFormatException;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import javax.measure.Measurable;
import javax.measure.Measure;
import javax.measure.quantity.Length;
import javax.measure.unit.NonSI;
import javax.measure.unit.SI;
import javax.measure.unit.Unit;
import org.apache.log4j.Logger;
import org.jdom.DocType;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.input.SAXBuilder;
import org.jdom.output.Format;
import org.jdom.output.XMLOutputter;
import org.jscience.economics.money.Currency;
import usda.weru.weps.location.StationMode;

/**
 * Contains configuration data for WepsUI.
 * 
 * @author wjr
 * @version 2.0
 */
public class ConfigData implements PropertyChangeListener {

    static{
        //The reference currency is always the United States Dollar.
        Currency.setReferenceCurrency(Currency.USD);
    }

    private static final Object LOCK = new Object();    
    public enum AccessLevel {

        Write,
        Read,
        Hidden;

        public static final AccessLevel parse(String name) {
            for (AccessLevel level : AccessLevel.values()) {
                if (level.toString().equalsIgnoreCase(name)) {
                    return level;
                }
            }
            return null;
        }

        public static final AccessLevel higher(AccessLevel a, AccessLevel b) {
            if (a == null && b == null) {
                return null;
            } else if (a == null) {
                return b;
            } else if (b == null) {
                return a;
            } else if (Write.equals(a) || Write.equals(b)) {
                return Write;
            } else if (Read.equals(a) || Read.equals(b)) {
                return Read;
            } else if (Hidden.equals(a) || Hidden.equals(b)) {
                return Hidden;
            } else {
                return null;
            }
        }
        
        public static final AccessLevel lower(AccessLevel a, AccessLevel b) {
            if (a == null && b == null) {
                return null;
            } else if (a == null) {
                return b;
            } else if (b == null) {
                return a;
            } 
            else if (Hidden.equals(a) || Hidden.equals(b)) {
                return Hidden;
            }             
            else if (Read.equals(a) || Read.equals(b)) {
                return Read;
            } 
            else if (Write.equals(a) || Write.equals(b)) {
                return Write;
            } 
            else {
                return null;
            }
        }
    }
    private static final Logger LOGGER = Logger.getLogger(ConfigData.class);
    private static final String XML_CONFIGURATION = "configuration";
    private static final String XML_VERSION = "version";
    private static final String XML_PARAMETER = "parameter";
    private static final String XML_NAME = "name";
    private static final String XML_VALUE = "value";
    private static final String XML_ACCESS = "access";
    private static final String CD = "CD-";
    private static final String RUNTIME_ONLY = "RUNTIME_ONLY";
    @FileConfigurationOption(defaultLocation="images")
    public static final String ReportFileName = CD + "report file name";
    public static final String Units = CD + "measurement";
    public static final String OutputFreq = CD + "output report frequency";
    // Submodel output flags
    public static final String OPHydroDet = CD + "detailed hydro output";
    public static final String OPSoilDet = CD + "detailed soil output";
    public static final String OPManageDet = CD + "detailed manage output";
    public static final String OPCropDet = CD + "detailed crop output";
    public static final String OPDecompDet = CD + "detailed decomp output";
    public static final String OPErosionDet = CD + "detailed erosion output";
    public static final String OPHydroDbg = CD + "debug hydro output";
    public static final String OPSoilDbg = CD + "debug soil output";
    public static final String OPManageDbg = CD + "debug manage output";
    public static final String OPCropDbg = CD + "debug crop output";
    public static final String OPDecompDbg = CD + "debug decomp output";
    public static final String OPErosionDbg = CD + "debug erosion output";

    // Windgen specific
    public static final String WindgenAllowedModes = CD + "windgen.allowedmodes";
    @FileConfigurationOption(defaultLocation="bin")
    public static final String WinExe = CD + "windgen generator";
    public static final String WinCmd = CD + "windgen cmdline";
    @FileConfigurationOption(defaultLocation="db/wind_gen")
    public static final String WinData = CD + "windgen database";
    @FileConfigurationOption(defaultLocation="db/wind_gen")
    public static final String WinIndex = CD + "windgen index";
//    @FileConfigurationOption(defaultLocation="db/wind_gen")
//    public static final String NRCSWinShape = CD + "nrcs wind shape file";
//    public static final String NRCSWinFlg = CD + "nrcs wind shape flg";
    @FileConfigurationOption(defaultLocation="bin")
    public static final String WindInterp1EXE = CD + "windgen interpolate 1";
    @FileConfigurationOption(defaultLocation="bin")
    public static final String WindInterp2EXE = CD + "windgen interpolate 2";

    // Cligen specific
    public static final String CligenAllowedModes = CD + "cligen.allowedmodes";
    @FileConfigurationOption(defaultLocation="bin")
    public static final String CliExe = CD + "cligen generator";
    public static final String CliCmd = CD + "cligen cmdline";
    @FileConfigurationOption(defaultLocation="db/cligen")
    public static final String CliData = CD + "cligen database";
    @FileConfigurationOption(defaultLocation="db/cligen")
    public static final String CliIndex = CD + "cligen index";
//    @FileConfigurationOption(defaultLocation="db/cligen")
//    public static final String NRCSCliShape = CD + "nrcs cli shape file";
//    public static final String NRCSCliFlg = CD + "nrcs cli shape flg";
    // Weps specfic
    @FileConfigurationOption(defaultLocation="bin")
    public static final String WepsExe = CD + "WEPS exe";
    public static final String WepsCmd = CD + "WEPS exe parameter";
    public static final String CalWepsCmd = CD + "WEPS calibration exe parameter";
    //user specific
    public static final String UserFullName = CD + "user.fullname";


    // Communications specific

    public static final String EmailSender = CD + "SenderEmail";
    
    public static final String UseOutlookFlg = CD + "Outlook Flg";
    public static final String MailHost = CD + "SMTP_host";
    public static final String CommentAddr = CD + "Dest_email";
    public static final String BugsAddr = CD + "Dest_bug_email";
    
    public static final String MantisEmail = CD + "MantisEmail";
    public static final String MantisURL = CD + "Mantis URL";
    public static final String MantisUser = CD + "Mantis user";
    public static final String MantisPassword = CD + "Mantis password";
    public static final String MantisProject = CD + "Mantis project";

    @FileConfigurationOption(defaultLocation="db")
    public static final String ManTemp = CD + "management template";
    @FileConfigurationOption(defaultLocation="db")
    public static final String ManTempSaveAs = CD + "management template save as";
    @FileConfigurationOption(defaultLocation="db")
    public static final String ManSkel = CD + "management skeleton";
    @FileConfigurationOption(defaultLocation="db")
    public static final String CropDB = CD + "cropdb";
    @FileConfigurationOption(defaultLocation=".")
    public static final String MCREW = CD + "MCREW";
    @FileConfigurationOption(defaultLocation="db")
    public static final String ManDB = CD + "manoperdb";
    @FileConfigurationOption(defaultLocation="db")
    public static final String SoilDB = CD + "soil database";
    //public static final String SoilDBSpec	= CD + "soil database spec";
    
    public static final String SoilDBDisp = CD + "soil database display";

    public static final String SoilReadonly= CD+"soil.readonly";

    @FileConfigurationOption(required=false, defaultLocation="db/soil")
    public static final String SoilOrganicFile = CD+"soil.organicfile";
    public static final String SoilTestOrganic = CD+"soil.organictest";
    @MeasureableConfigurationOption(units="mm", alternativeUnits="in")
    public static final String SoilMaxOrganicDepth = CD+"soil.maxorganicdepth";

    @FileConfigurationOption
    public static final String ProjDir = CD + "projects path";
    @FileConfigurationOption(parentOption=MCREW)
    public static final String McrewDataConfigFile = CD + "mcrew data config file name";
    public static final String TimeSteps = CD + "time steps";
    public static final String UseMap = CD + "use map";
    public static final String DispLatLon = CD + "display latitude longitude";
    public static final String DispElevation = CD + "display elevation";
    public static final String DispStCty = CD + "display state county";
    public static final String ReadonlySlope = CD + "readonly region slope";
    public static final String ReadonlyRockFragments = CD + "readonly rock fragments";
    public static final String McrewEdit = CD + "mcrew edit";
    public static final String SoilEstimate = CD + "soil estimate";
    public static final String SoilAverageStratifiedLayers = "CD-soil.averagestratified";
    public static final String SoilSkipOrganicSurfaceLayers = "CD-soil.skiporganic";
    public static final String ShowWeatheFilerCheckboxes = CD + "showweatherfilecheckboxes";
    public static final String WindFlag = CD + "wind flag";

    @MeasureableConfigurationOption(units="km", alternativeUnits="mi")
    public static final String WindRadius = CD + "wind radius";
    public static final String ClimateFlag = CD + "climate flag";

    @MeasureableConfigurationOption(units="km", alternativeUnits="mi")
    public static final String ClimateRadius = CD + "climate radius";
    public static final String SubDailyFlag = CD + "sub-daily flag";
    //public static final String RunTypeDisp = CD + "runtypedisp";
    public static final String DefaultRunMode = CD + "defaultrunmode";
    public static final String NRCSRunLen = CD + "nrcsrunlength";
    public static final String TTInit = CD + "ToolTipInit";
    public static final String TTDismiss = CD + "ToolTipDismiss";    // CurrentProj is a CD entry but its stored value is not used
    @FileConfigurationOption()
    public static final String CurrentProj = CD + "current project";
    public static final String SWEEP = CD + "SWEEP-";
    @FileConfigurationOption(defaultLocation="bin")
    public static final String SWEEPExe = SWEEP + "exe";
    public static final String SWEEPCmd = SWEEP + "cmdLine";
    public static final String SWEEPWorkDir = SWEEP + "work";
    //public static final String FireProperty = "FireProperty " + CD;
    public static final String DataChanged = "data changed " + CD;
    public static final String Dates = "dates";
    public static final String Cycle = "cycle";
    public static final String NRCS = "NRCS";
    //Formats for the display of data
    public static final String FormatOperationDate = CD + "operation date format";
    //Runs Location default
    @FileConfigurationOption(required=false)
    public static final String DefaultRunsLocationTemplate = CD + "default runs location Template";
    public static final String DefaultRunsLocation = RUNTIME_ONLY + CD + "default runs location";
    //Files to purge after running a simulation.
    public static final String PurgeRunFiles = CD + "purge.filefilter";
    //Flag to indicate that .cli & .win files should be deleted from run directory
    public static final String PurgeRunFilesFlg = CD + "purge.flag";
    //Mcrew's skel translation file
    @FileConfigurationOption()
    public static final String SkelTranslationFile = CD + "skel translations file";
    //Don't warn about system locale
    public static final String DoNotWarnAboutSystemLocale = CD + "don't warn about system locale";
    //Don't estimate missing surgo values
    public static final String DoNotEstimateMissingSurgoValues = CD + "do not estimate missing surgo values";
    public static final String OMFractionThreshold = CD + "organic material fraction threshold";
    //Use the system default mailer
    public static final String UseDefaultMailClient = CD + "use default mail client";
    public static final String DoNotReviewWarningsWhenRestoringRun = CD + "do not review warnings when restoring run";
    @FileConfigurationOption(defaultLocation="tables")
    public static final String DetailTableFilterFile = CD + "detail table filter file";
    public static final String SingleProjectMode = CD + "single project mode";
    public static final String HideProjectFileButtons = CD + "hide project file buttons";
    public static final String HideTemplatetFileButtons = CD + "hide template file buttons";
    public static final String DaysToKeepLogs = CD + "logs.daystokeep";
    public static final String ReportsView = CD + "reports.view";
    public static final String ReportsGeneratePDF = CD + "reports.generatepdf";
    @FileConfigurationOption(required=false)
    public static final String ReportsDirectory= CD + "reports.directory";
    public static final String ReportsCustomized= CD + "reports.customized";
    public static final String AllowScriptCreation = CD + "script.allow";
    public static final String CompatibilityNRCS = CD + "compatibility.nrcs";
    public static final String BarriersReadonly = CD + "barriers.readonly";
    @FileConfigurationOption()
    public static final String BarriersFile = CD + "barriers.file";

    @FileConfigurationOption()
    public static final String GISData = CD + "gis.data";

    public static final String SiteChooserShowCountry =CD + "sitechooser.showcountry";

    //enable experimental xml format
    public static final String FormatsXML =CD + "formats.xml";

    //fules
    @FileConfigurationOption
    public static final String FuelDatabase = CD + "fuel.database";

    public static final String FuelDefault = CD + "fuel.default";

    private static ConfigData c_default;
    private List<ConfigStorage> c_storage;
    private ConfigStorage c_mainConfig;
    private ConfigStorage c_userConfig;
    private ConfigStorage c_defaultConfig;
    private Map<String, AccessLevel> c_accessLevels;
    public static final String PROP_CHANGED = "changed";
    private boolean c_changed = true;
    /**
     * ConfigData throws a property change each time setData is called.
     * Wrapper classes listen to determine if the the event is relevant to
     * them.
     */
    private PropertyChangeSupport c_changes = new PropertyChangeSupport(this);

    private ConfigData() {
        //TODO: does this really make sense to listen to itself?
        c_changes.addPropertyChangeListener(this);


        //default data
        synchronized(LOCK){
            c_storage = new ArrayList<ConfigStorage>();
            //special storage with all the default values
            c_defaultConfig = new DefaultConfigStoage();
            c_defaultConfig.load();
            c_storage.add(c_defaultConfig);
                        
        }

    }
    
    private void fixUp(){
        //if there is no cligen index then use the cligen data and replace the exention with .idx
        String cliIndex = getDataParsed(CliIndex);
        if(cliIndex == null || cliIndex.trim().length() == 0){
            //no cligen index, build from the clidata value
            String data = getDataParsed(CliData);
            if (data != null && data.trim().length() > 0){
                cliIndex = Util.purgeExtensions(data, ".par") + ".idx";
                setData(CliIndex, cliIndex);
            }
        }

        //if there is no wingen index then use the windgen data and replace the exention with .idx
        String winIndex = getDataParsed(WinIndex);
        if(winIndex == null || winIndex.trim().length() == 0){
            //no cligen index, build from the clidata value
            String data = getDataParsed(WinData);
            if (data != null && data.trim().length() > 0){
                winIndex = Util.purgeExtensions(data, ".wdb") + ".idx";
                setData(WinIndex, winIndex);
            }
        }
    }

    private void applyCompatibility(){
        //are we in nrcs compatibility mode?
        if ("1".equals(getData(CompatibilityNRCS))){
            //yes, nrcs install
            setData(DefaultRunMode, ConfigData.NRCS);
            
            //user is not allowed to change the modes.            
            
            setAccessLevel(DefaultRunMode, AccessLevel.lower(getAccessLevel(DefaultRunMode), AccessLevel.Read));
            setAccessLevel(NRCSRunLen, AccessLevel.lower(getAccessLevel(NRCSRunLen), AccessLevel.Read));

            //reports
            setData(ReportsCustomized, "nrcs");
        }
    }

    public static ConfigData getDefault() {
        if (c_default == null) {
            c_default = new ConfigData();
        }
        return c_default;
    }

    public void load(File main, File user) {
        c_storage = new ArrayList<ConfigStorage>();

        //main config
        if (main != null && main.exists()) {
            LOGGER.info("Config File: \"" + main.getAbsolutePath() + "\"");
            synchronized(LOCK){
                c_mainConfig = new ConfigStorage(main, true);
                c_mainConfig.load();
                c_storage.add(c_mainConfig);
            }
        } else {
            LOGGER.warn("Config File: NULL");
            LOGGER.warn("Using a default main configuration.");
        }

        //user's config
        if (user != null) {
            synchronized(LOCK){
                LOGGER.info("User Config File: \"" + user.getAbsolutePath() + "\"");
                c_userConfig = new ConfigStorage(user, false);
                c_userConfig.load();
                c_storage.add(0, c_userConfig);
            }
        } else {
            LOGGER.info("User Config File: NULL");
        }

        //special storage with all the default values
        c_storage.add(c_defaultConfig);
        applyCompatibility();
        fixUp();
    }
    
    public File getMainFile(){
        return c_mainConfig.c_file;
    }
    
    public File getUserFile(){
        return c_userConfig.c_file;
    }

    public AccessLevel getAccessLevel(String propertyName) {
        AccessLevel level = null;
        if (c_accessLevels != null) {
            level = c_accessLevels.get(propertyName);
        }
        return level != null ? level : AccessLevel.Write;
    }
    
    protected void setAccessLevel(String propertyName, AccessLevel level){
        if (c_accessLevels == null) {
            c_accessLevels = new HashMap<String, AccessLevel>();
        }
        c_accessLevels.put(propertyName, level);
    }

    public boolean isChanged() {
        return c_changed;
    }

    /**
     * Gets data from hashtable.
     * 
     * @param propertyName Specifies which datum to retrieve using public static final String parameters.     
     * @return 
     */
    public String getData(String propertyName) {
        synchronized(LOCK){
            for (ConfigStorage storage : c_storage) {
                String value = storage.get(propertyName);
                if (value != null) {
                    return value;
                }
            }
            return null;
        }
    }
    
    public String getDataParsed(String propertyName){
        return Util.parse(getData(propertyName));
    }

    /**
     * Puts data into hashtable.
     * 
     * @param propertyName Specifies which datum to save using public static final String parameters.
     * @param newData Value of datum saved.
     */
    public void setData(String propertyName, String newData) {
        synchronized(LOCK){
            if (c_userConfig == null) {
                //no user config can't set values, this could break things!
                return;
            }
        }

        if (newData != null) {
            newData = newData.trim();
        } else {
            newData = "";
        }

        String oldData = getData(propertyName);
        if (newData.equals(oldData)) {
            //no change, leaving
            return;
        }

        //there is a change, do we have permission
        AccessLevel level = getAccessLevel(propertyName);

        //we only need permission on CD properties, others are not saved to file
        if (!CurrentProj.equals(propertyName) && propertyName.startsWith(CD) && !AccessLevel.Write.equals(level)) {
            //we don't have access level to change this value.
            return;
        }
        
        synchronized(LOCK){
            c_userConfig.set(propertyName, newData);
        }

        c_changes.firePropertyChange(propertyName, oldData, newData);


        if (propertyName.equals(DefaultRunsLocationTemplate)) {
            updateDefaultRunsLocation();
        }
        if (propertyName.equals(CurrentProj)) {
            updateDefaultRunsLocation();
        }
    }

    private Collection<String> keys() {
        synchronized(LOCK){
            List<String> temp = new LinkedList<String>();
            for (ConfigStorage storage : c_storage) {
                for (String key : storage.keys()) {
                    if (!temp.contains(key)) {
                        temp.add(key);
                    }
                }
            }
            return temp;
        }
    }

    public void fireAll() {
        boolean firedDefaultRunLocation = false;
        for (String key : keys()) {
            if (DefaultRunsLocation.equals(key)) {
                firedDefaultRunLocation = true;
            }
            String value = getData(key);
            c_changes.firePropertyChange(key, null, value);
        }

        if (!firedDefaultRunLocation) {
            updateDefaultRunsLocation();
        }
    }

    public void fireAll(PropertyChangeListener... listeners) {
        boolean firedDefaultRunLocation = false;
        for (String key : keys()) {
            if (DefaultRunsLocation.equals(key)) {
                firedDefaultRunLocation = true;
            }
            String value = getData(key);
            PropertyChangeEvent event = new PropertyChangeEvent(this,key, null, value);
            for (PropertyChangeListener listener : listeners) {
                listener.propertyChange(event);
            }
        }
        if (!firedDefaultRunLocation) {
            updateDefaultRunsLocation();
        }
    }

    public void save() {
        if (c_userConfig != null) {
            c_userConfig.save();
            boolean old = c_changed;
            c_changed = false;
            c_changes.firePropertyChange(PROP_CHANGED, old, false);
        } else {
            LOGGER.warn("User config is null.  Unable to save configuration.");
        }
    }

    /**
     * 
     * @param l 
     */
    public void addPropertyChangeListener(PropertyChangeListener l) {
        c_changes.addPropertyChangeListener(l);
    }

    /**
     * 
     * @param l 
     */
    public void removePropertyChangeListener(PropertyChangeListener l) {
        c_changes.removePropertyChangeListener(l);
    }

    /**
     * 
     * @param l 
     */
    public void addPropertyChangeListener(String propertyName, PropertyChangeListener l) {
        c_changes.addPropertyChangeListener(propertyName, l);
    }

    /**
     * 
     * @param l 
     */
    public void removePropertyChangeListener(String propertyName, PropertyChangeListener l) {
        c_changes.removePropertyChangeListener(propertyName, l);
    }

    /**
     * 
     * @param e 
     */
    public void propertyChange(PropertyChangeEvent e) {
        String propertyName = e.getPropertyName();
        if(propertyName == null){
            return;
        }
        if (!propertyName.startsWith(CD)) {
            return;
        }
        String valstr = (String) e.getNewValue();
        setData(propertyName, valstr);

        if (propertyName.startsWith(CD)) {
            boolean old = c_changed;
            c_changed = true;
            c_changes.firePropertyChange(PROP_CHANGED, old, true);
        }

    }



    private void updateDefaultRunsLocation() {
        try {
            String template = getData(DefaultRunsLocationTemplate);
            template = Util.parse(template);
            File file = new File(template);
            String parsedPath = file.getAbsolutePath();

            //Update listeners to the parsed template path
            setData(DefaultRunsLocation, parsedPath);
        } catch (NullPointerException e) {
        }
    }

    private class ConfigStorage {

        protected Map<String, String> c_data;
        private File c_file;
        private boolean c_readAccessLevels;

        protected ConfigStorage() {
        }

        public ConfigStorage(File file, boolean access) {
            this();
            c_file = file;
            c_readAccessLevels = access;
        }

        public Collection<String> keys() {
            synchronized (this) {
                if (c_data == null) {
                    load();
                }
            }
            return c_data.keySet();
        }

        public String get(String propertyName) {
            synchronized (this) {
                if (c_data == null) {
                    load();
                }
            }
            return c_data.get(propertyName);
        }

        public void set(String propertyName, String value) {
            synchronized (this) {
                if (c_data == null) {
                    load();
                }
            }
            c_data.put(propertyName, value);
        }

        protected void load() {
            try {                
                synchronized (this) {
                    c_data = new HashMap<String, String>();
                }  

                if (!c_file.exists()) {
                    return;
                }

                SAXBuilder builder = new SAXBuilder();
                FileInputStream in = new FileInputStream(c_file);
                Document document = builder.build(in);

                Element root = document.getRootElement();
                for (Object o : root.getChildren(XML_PARAMETER)) {
                    Element parameter = (Element) o;
                    String key = parameter.getChildText(XML_NAME);
                    String value = parameter.getChildText(XML_VALUE);

                    //UPGRADE PATHS
                    //ignore the runtypedisp value from the config, this is only in the runfiledata now
                    if ("CD-runtypedisp".equals(key)){
                        LOGGER.info("Parameter \"" + key + "\" deprecated  use " + DefaultRunMode);
                        continue;
                    }
                    //END UPGRADE PATHS

                    //read and store the access level
                    if (c_readAccessLevels) {
                        //stor the value
                        synchronized (this) {
                            c_data.put(key, value != null ? value : "");
                        }

                        String levelText = parameter.getChildText(XML_ACCESS);
                        AccessLevel level = AccessLevel.parse(levelText);
                        setAccessLevel(key, level);
                        if (level != null) {
                            setAccessLevel(key, level);
                        }
                    } else {
                        //check access level before storing
                        if (CurrentProj.equals(key) || AccessLevel.Write.equals(getAccessLevel(key))) {
                            synchronized (this) {
                                c_data.put(key, value != null ? value : "");
                            }
                        } else {
                            LOGGER.warn("Parameter \"" + key + "\" ignored from user config due to access restrictions.");
                        }

                    }
                }
                return;
            } catch (UTFDataFormatException udfe) {
                LOGGER.error("Unable to load XML configuration file." + c_file.getPath(), udfe);
                return;
            } catch (JDOMException jde) {
                if (jde.getMessage().indexOf("Content is not allowed in prolog") > 0) {
                    return;
                }//Assume this is not an xml file.

                LOGGER.error("Unable to load XML configuration file. " + c_file.getPath(), jde);
                return;
            } catch (IOException ioe) {
                LOGGER.error("Unable to load XML configuration file." + c_file.getPath(), ioe);
                return;
            }
        }
                

        protected void save() {
            DocType type = new DocType(XML_CONFIGURATION);
            String dtd = "<!ELEMENT configuration (parameter*)>";
            dtd += "<!ELEMENT parameter (name, value)>";
            dtd += "<!ELEMENT name (#PCDATA)>";
            dtd += "<!ELEMENT value (#PCDATA)>";
            dtd += "<!ATTLIST configuration version CDATA #REQUIRED>";
            type.setInternalSubset(dtd);
            Element root = new Element(XML_CONFIGURATION);
            Document document = new Document(root, type);

            root = document.getRootElement().setAttribute(XML_VERSION, "1.0");
            
            
            for (String key : keys()) {
                String value = get(key);
                AccessLevel level = getAccessLevel(key);

                //Special case, we write current project even if it's hidden.
                if ( !CurrentProj.equals(key) && !AccessLevel.Write.equals(level)) {
                    //skip ones that we aren't allowed to write
                    continue;
                }

                //only save config values
                if (!key.startsWith(CD)) {
                    continue;
                }

                //only save if different from main
                synchronized(LOCK){
                    String main = c_mainConfig.get(key);
                        if (areStringsEqual(main, value)) {
                            continue;
                        }
                }
                

                Element node = new Element(XML_PARAMETER);
                root.addContent(node);

                Element nameNode = new Element(XML_NAME);
                nameNode.setText(key);
                node.addContent(nameNode);

                Element valueNode = new Element(XML_VALUE);
                valueNode.setText(value);
                node.addContent(valueNode);               
                
                
                
                

            }

            //Write the XML File
            try {
                //setup the folder structure if needed.
                java.io.File parent = c_file.getParentFile();
                if (parent != null && !parent.exists()) {
                    if (!parent.mkdirs()) {
                        if (!parent.exists()) {
                            throw new IOException("can't create directory");
                        }
                    }
                }

                FileWriter writer = new FileWriter(c_file);
                XMLOutputter serializer = new XMLOutputter();
                serializer.setFormat(Format.getPrettyFormat());
                serializer.output(document, writer);
                writer.close();
            } catch (IOException ioe) {
                LOGGER.error("Error writing config file \"" + c_file.getPath() + "\".", ioe);
            }
        }
    }
    //special stoage that has all the default values, sits at the bottom of the stack
    private class DefaultConfigStoage extends ConfigStorage {

        @Override
        protected void load() {
            synchronized (this) {
                c_data = new HashMap<String, String>();
            }            

            set(Units, Util.SIUnits);
            set(OutputFreq, "2");
            set(OPHydroDet, "0");
            set(OPSoilDet, "0");
            set(OPManageDet, "0");
            set(OPCropDet, "0");
            set(OPDecompDet, "0");
            set(OPErosionDet, "0");
            set(OPHydroDbg, "0");
            set(OPSoilDbg, "0");
            set(OPManageDbg, "0");
            set(OPCropDbg, "0");
            set(OPDecompDbg, "0");
            set(OPErosionDbg, "0");
            set(WinExe, "");
            set(WinCmd, "");
            set(WinData, "db/wind_gen/wind_gen_his_upper_US.wdb");
            set(CliExe, "");
            set(CliCmd, "");
            set(CliData, "db/cligen/US_cligen_stations.par");
            set(WepsExe, "");
            set(WepsCmd, "");
            set(CalWepsCmd, "");
            set(EmailSender, "");
            set(MailHost, "");
            set(CommentAddr, "");
            set(BugsAddr, "");
            set(ManTemp, "");
            set(ManTempSaveAs, "");
            set(ManSkel, "");
            set(CropDB, "");
            set(ManDB, "");
            set(SoilDB, "");
            set(SoilOrganicFile,"db/soil/NRCS Generic Soils/Organic Soil.ifc");
            set(SoilMaxOrganicDepth, "101.6");
            set(SoilTestOrganic, "1");
            set(ProjDir, "");
            set(MCREW, "mcrew_cfg");
            set(McrewDataConfigFile, "dataconfig.xml");
            set(TimeSteps, "24");
            set(WindFlag, "0");
            set(ClimateFlag, "0");
            set(SubDailyFlag, "0");
            set(DefaultRunMode, ConfigData.Cycle);
            set(TTInit, "2000");		// set to more reasonable defaults if not
            set(TTDismiss, "2000");		// set in cfg file (or cfg file is missing)
            set(ReadonlySlope, "1");
            set(ReadonlyRockFragments, "1");

            //Formats
            set(FormatOperationDate, "MMM dd, y");

            //Purge run files
            set(PurgeRunFilesFlg, "0");
            set(PurgeRunFiles, "win_gen.win:cli_gen.cli");

            //Default runs location template
            set(DefaultRunsLocationTemplate, "${PROJECT_DIRECTORY}");

            //Mcrew's skel translastion file
            set(SkelTranslationFile, "${weps.databases}/db/RUSLE2_Translation.txt");

            set(DoNotWarnAboutSystemLocale, "0");

            set(DoNotEstimateMissingSurgoValues, "0");

            set(UseDefaultMailClient, "0");

            set(DoNotReviewWarningsWhenRestoringRun, "0");
            set(SoilEstimate, "1");
            set(SoilAverageStratifiedLayers, "0");
            set(OMFractionThreshold, "0.20");

            set(DetailTableFilterFile, "tables/detail_filters.xml");

            set(HideProjectFileButtons, "0");
            set(HideTemplatetFileButtons, "0");
            set(DaysToKeepLogs, "2");
            
            
            //reports 
            set(ReportsView, "run");
            set(ReportsGeneratePDF, "run,management,crop,stir");            
            set(ReportsDirectory, "reports/");
            set(ReportsCustomized, null);
            
            //bash script 
            set(AllowScriptCreation, "1");
            
            //nrcs install
            set(CompatibilityNRCS, "0");

            //default barriers
            set(BarriersFile, "db/barriers/barrier.dat");

            ///barriers readonly, false
            set(BarriersReadonly, "0");

            //show the checkboxes that allow switching between weather files or station lists on a per project basis
            set(ShowWeatheFilerCheckboxes, "1");

            //LocationPanel rewrite
            set(WindgenAllowedModes, "choice,file,interpolated,gis,nearest,nrcs");
            set(CligenAllowedModes, "choice,nearest,gis,file,nrcs");

            setData(SoilAverageStratifiedLayers, "0");
            setData(SoilSkipOrganicSurfaceLayers, "0");
            setData(SiteChooserShowCountry, "0");

            set(GISData, "db/gis");

            set(SoilReadonly, "0");

            //default to not allowing xml format
            set(FormatsXML, "0");

            //fuels
            set(FuelDatabase, "${weps.databases}/db/fuels/fuels.xml");
            set(FuelDefault,  "diesel");

        }

        @Override
        protected void save() {
            //do nothing
        }
    }

    private static boolean areStringsEqual(String a, String b) {
        if (a == null && b == null) {
            return true;
        } else if (a != null && b != null) {
            return a.equals(b);
        } else {
            return false;
        }
    }

    public boolean getSoilTestOrganic(){
        return "1".equals(getData(SoilTestOrganic));
    }

    public Measurable<Length> getSoilMaxOrganicDepth(){
        String valueString = getData(SoilMaxOrganicDepth);
        try{
            double depthMiliMeters = Double.valueOf(valueString);
            return Measure.valueOf(depthMiliMeters, SI.MILLIMETER);        
        }
        catch(Exception e){
            LOGGER.warn("Unable to parse the soil max organic depth. Defaulting to 4 inches." + valueString);
            return Measure.valueOf(4, NonSI.INCH);
        }
    }

    public boolean isAverageStratifiedSoilLayers(){
        String value = getData(SoilAverageStratifiedLayers);
        return value != null ? value.trim().equals("1") : false;
    }

        public boolean isSkipOrganicSoilSurfaceLayers(){
        String value = getData(SoilSkipOrganicSurfaceLayers);
        return value != null ? value.trim().equals("1") : false;
    }

    //TODO: fire changes when the underlying data changes
    public static final String PROP_ALLOWED_CLIGEN_STATION_MODES = "allowedCligenStationModes";
    public StationMode[] getAllowedCligenStationModes(){
        return parseAllowedStationModes(getData(CligenAllowedModes));
    }

    public static final String PROP_ALLOWED_WINDGEN_STATION_MODES = "allowedWindgenStationModes";
    public StationMode[] getAllowedWindgenStationModes(){
        return parseAllowedStationModes(getData(WindgenAllowedModes));
    }

    public static final String PROP_CLIGEN_SEARCH_RADIUS= "cligenSearchRadius";
    public Measurable<Length> getCligenSearchRadius(){
        String valueString = getData(ClimateRadius);
        try {            
            double distanceMeters = Double.parseDouble(valueString.trim());
            return Measure.valueOf(distanceMeters, SI.KILOMETER);
        } catch (Exception e) {
            LOGGER.warn("Unable to parse the cligen radius limit. " + valueString);
            return null;
        }
    }

    public static final String PROP_CLIGEN_SEARCH_LIMIT= "cligenSearchLimit";
    public int getCligenSearchLimit(){
        return 50;
    }

    public static final String PROP_WINDGEN_SEARCH_RADIUS= "windgenSearchRadius";
    public Measurable<Length> getWindgenSearchRadius(){
        String valueString = getData(WindRadius);
        try {            
            double distanceMeters = Double.parseDouble(valueString.trim());
            return Measure.valueOf(distanceMeters, SI.KILOMETER);
        } catch (Exception e) {
            LOGGER.warn("Unable to parse the windgen radius limit. " + valueString);
            return null;
        }
    }

    public static final String PROP_WINDGEN_SEARCH_LIMIT= "windgenSearchLimit";
    public int getWindgenSearchLimit(){
        return 50;
    }

    private static final StationMode[] parseAllowedStationModes(String modes){
        if(modes == null || modes.trim().length()==0){
            return new StationMode[0];
        }
        EnumSet <StationMode> c_allowedModes = null;
        String[] parts = modes.split(",");
        for(String part : parts){
            StationMode mode = StationMode.parse(part.trim());
            if(mode != null){
                if(c_allowedModes == null){
                    c_allowedModes = EnumSet.of(mode);
                }
                else{
                    c_allowedModes.add(mode);
                }
            }
        }

        return c_allowedModes.toArray(new StationMode[c_allowedModes.size()]);
    }

    public void logConfiguration(){
        try{
            StringBuffer buffer = new StringBuffer();
            List<String> sortedKeys = new LinkedList<String> (keys());
            Collections.sort(sortedKeys);
            for(String key : sortedKeys){
                //USER
                String value = c_userConfig.get(key);
                String type = "user";

                //MAIN
                if(value == null){
                    value = c_mainConfig.get(key);
                    type = "main";
                }

                //DEFAULT
                if(value == null){
                    value = c_defaultConfig.get(key);
                    type = "default";
                }

                if(value == null){
                    value = "null";
                    type = null;
                }

                buffer.append("\t" + key + (type != null ? "(" + type + ")" : "")+ "= " + value + "\n");

                String valueParsed = Util.parse(value);
                if(!value.equals(valueParsed)){
                    buffer.append("\t" + key + "(parsed) = " + valueParsed + "\n");
                }


            }

            LOGGER.debug("Current Configuration:\n" + buffer.toString());
        }
        catch(Exception e){
            LOGGER.warn("Unable to log configuration.", e);
        }
    }

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.FIELD)
    public static @interface MeasureableConfigurationOption{        
        String units();
        String alternativeUnits();
        boolean isDouble() default true;

    }

    private Map<String, MeasureableConfigurationOption> c_measureableConfigurationOptions;

    private synchronized Map<String, MeasureableConfigurationOption> measureableOptions(){
        if(c_measureableConfigurationOptions == null){
            c_measureableConfigurationOptions = new HashMap<String, MeasureableConfigurationOption>();
             //loop over all the fields in the configdata and record all those with the FileConfigurationOption annotation
            for (Field field : ConfigData.class.getFields()) {
                try {
                    //if final static and annotated
                    if (Modifier.isStatic(field.getModifiers()) && Modifier.isFinal(field.getModifiers()) && field.isAnnotationPresent(MeasureableConfigurationOption.class)) {
                        MeasureableConfigurationOption annotation = field.getAnnotation(MeasureableConfigurationOption.class);
                        String propertyName = field.get(null).toString();
                        c_measureableConfigurationOptions.put(propertyName, annotation);
                    }
                } catch (Exception e) {
                    LOGGER.error("Initilizing", e);
                }
            }
        }
        return c_measureableConfigurationOptions;
    }

    public MeasureableConfigurationOption getMeasureableConfigurationOption(String propertyName){
        return measureableOptions().get(propertyName);
    }

    public Unit getMeasureableUnit(String propertyName){
        MeasureableConfigurationOption option = getMeasureableConfigurationOption(propertyName);

        if(option != null){           
                Unit units = Unit.valueOf(option.units());                
                return units;           
        }
        return null;
    }

        public Unit getMeasureableAlternativeUnit(String propertyName){
        MeasureableConfigurationOption option = getMeasureableConfigurationOption(propertyName);

        if(option != null){
                Unit units = Unit.valueOf(option.alternativeUnits());
                return units;
        }
        return null;
    }


    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.FIELD)
    public static @interface FileConfigurationOption{
        boolean required() default true;
        String parentOption() default "";
        String defaultLocation() default "";
    }

    private Map<String, FileConfigurationOption> c_fileConfigurationOptions;

    private synchronized Map<String, FileConfigurationOption> fileOptions(){
        if(c_fileConfigurationOptions == null){
            c_fileConfigurationOptions = new HashMap<String, FileConfigurationOption>();
             //loop over all the fields in the configdata and record all those with the FileConfigurationOption annotation
            for (Field field : ConfigData.class.getFields()) {
                try {
                    //if final static and annotated
                    if (Modifier.isStatic(field.getModifiers()) && Modifier.isFinal(field.getModifiers()) && field.isAnnotationPresent(FileConfigurationOption.class)) {
                        FileConfigurationOption annotation = field.getAnnotation(FileConfigurationOption.class);
                        String propertyName = field.get(null).toString();
                        c_fileConfigurationOptions.put(propertyName, annotation);
                    }
                } catch (Exception e) {
                    LOGGER.error("Initilizing", e);
                }
            }
        }
        return c_fileConfigurationOptions;
    }
    
    public boolean isFileConfigurationOption(String propertyName){
        return fileOptions().containsKey(propertyName);
    }

    public FileConfigurationOption getFileConfigurationOption(String propertyName){
        return fileOptions().get(propertyName);
    }

    public String[] getFileConfigurationOptions(){
        Collection<String> temp = fileOptions().keySet();
        return temp.toArray(new String[temp.size()]);
    }

    public boolean isFileReferenceErrorInConfigData(){
        boolean failed = false;
        for(Map.Entry<String, FileConfigurationOption> entry : fileOptions().entrySet()){
            //exit on the first error to be as fast as possible
            String propertyName = entry.getKey();
            FileConfigurationOption meta = entry.getValue();
            if(getAccessLevel(propertyName) != AccessLevel.Hidden){
                if(meta.required()){
                                //test if the value exists as a file
                    File parentFile = null;
                    String parentPropertyName = meta.parentOption();
                    if(parentPropertyName != null && parentPropertyName.trim().length() > 0){
                        String parentPath = getDataParsed(parentPropertyName);
                        if(parentPath != null){
                            parentFile = new File(Util.parse(parentPath));
                        }
                    }

                    String path = getDataParsed(propertyName);
                    boolean error = false;
                    if(path != null){
                        File file = new File(path);
                        if(!file.isAbsolute() && parentFile != null){
                             file = new File(parentFile, Util.parse(path));
                        }                        

                        error = !file.exists();
                    }
                   if(error){
                       failed = true;
                       LOGGER.warn("Unable to resolve file reference for : " + propertyName + "=" + getData(propertyName));
                   }
                }
            }
        }
        return failed;
    }

    public boolean isSiteChooserShowCountries(){
        return "1".equals(getData(SiteChooserShowCountry));
    }

    public boolean isPurgeFlag(){
        return "1".equals(getData(PurgeRunFilesFlg));
    }

    public boolean isSoilReadonly(){
        return "1".equals(getData(SoilReadonly));
    }

   public boolean isFormatXMLAllowed(){
        return "1".equals(getData(FormatsXML));
    }


    public String[] getPurgeFilenames(){
        String filter = getData(PurgeRunFiles);
        filter = filter != null ? filter.trim() : null;

        if(filter == null || filter.length() == 0){
            return new  String[0];
        }

        String[] parts = filter.split(":");
        for (int i = 0; i < parts.length; i++){
            parts[i] = parts[i].trim();
        }
        return parts;
    }

    /**
     * Always returns USD.  Placeholder for future potential support of other
     * currencies.
     * @return USD
     */
    public Currency getCurrency(){
        return Currency.USD;
    }
    
    public boolean isCompatibilityNRCS(){
        return "1".equals(getData(CompatibilityNRCS));
    }

}

