package org.limewire.setting;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Properties;

import org.limewire.setting.evt.SettingEvent;
import org.limewire.setting.evt.SettingListener;
import org.limewire.setting.evt.SettingEvent.EventType;


/**
 * Provides a key-value property as an abstract class. The value is typed
 * in subclasses to avoid casting and ensure your settings are type-safe.
 * The value has a unique key.
 * <p>
 * When you add a new <code>AbstractSetting</code> subclass, add a public synchronized 
 * method to {@link SettingsFactory} to create an instance of the setting. 
 * For example, subclass {@link IntSetting}, <code>SettingsFactory</code> has 
 * {@link SettingsFactory#createIntSetting(String, int)} and
 * {@link SettingsFactory#createRemoteIntSetting(String, int, String, int, int)}.
 * <p>
 * <code>AbstractSetting</code> includes an abstract method to load a <code>String</code>
 * into the key-value property. You are responsible to convert the 
 * <code>String</code> to the appropriate type in a subclass. 
 * <p>
 * For example, if your subclass is for an integer setting you can
 * have a integer field i.e. <code>myIntValue</code>, in the class. Then you 
 * would set <code>myIntValue</code> with the integer converted 
 * <code>String<code> argument, for example: 
 <pre> 
   protected void loadValue(String sValue) {
        try {
            value = Integer.parseInt(sValue.trim());
        } catch(NumberFormatException nfe) {
            revertToDefault();
        }
    }
 </pre>
 *<p>
 * This class, includes fields for the <code>AbstractSetting</code>'s visibility 
 * (public vs. private) and persistence (always save vs don't save). 
 * <p>
 * Visibility and persistence are just fields for a property; what the field 
 * means to your application is up to you. For example, you could give the 
 * setting a "don't save" value and when it's time to store the setting to a 
 * database, you check the setting and take appropriate actions.
 * <p>
 * See {@link SettingsFactory} for an example of creating an 
 * <code>IntSetting</code> object which is a
 * subclass of <code>AbstractSetting</code> . Additionally the
 * example shows how to load and save the setting to disk.
 */
public abstract class AbstractSetting implements Setting {
    
	/**
	 * Protected default <tt>Properties</tt> instance for subclasses.
	 */
	protected final Properties DEFAULT_PROPS;

	/**
	 * Protected <tt>Properties</tt> instance containing properties for any
	 * subclasses.
	 */
	protected final Properties PROPS;

	/**
	 * The constant key for this property, specified upon construction.
	 */
	protected final String KEY;

	/**
	 * Constant for the default value for this <tt>Setting</tt>.
	 */
	protected final String DEFAULT_VALUE;
    
	/**
	 * Value for whether or not this setting should always save.
	 */
	private boolean alwaysSave = false;
	
	/**
	 * Setting for whether or not this setting is private and should
	 * not be reported in bug reports.
	 */
	private boolean isPrivate = false;

	/**
	 * List of {@link SettingListener}
	 */
    private Collection<SettingListener> listeners = null;
    
	/**
	 * Constructs a new setting with the specified key and default
	 * value.  Private access ensures that only this class can construct
	 * new <tt>Setting</tt>s.
	 *
	 * @param key the key for the setting
	 * @param defaultValue the defaultValue for the setting
	 * @throws <tt>IllegalArgumentException</tt> if the key for this 
	 *  setting is already contained in the map of default settings
	 */
	protected AbstractSetting(Properties defaultProps, Properties props, String key, 
                String defaultValue) {
		DEFAULT_PROPS = defaultProps;
		PROPS = props;
		KEY = key;
		DEFAULT_VALUE = defaultValue;
		if(DEFAULT_PROPS.containsKey(key)) 
			throw new IllegalArgumentException("duplicate setting key: " + key);
		DEFAULT_PROPS.put(KEY, defaultValue);
        loadValue(defaultValue);
	}
    
	/* (non-Javadoc)
     * @see org.limewire.setting.Setting#addSettingListener(org.limewire.setting.evt.SettingListener)
     */
	public void addSettingListener(SettingListener l) {
        if (l == null) {
            throw new NullPointerException("SettingListener is null");
        }
        
        synchronized (this) {
            if (listeners == null) {
                listeners = new ArrayList<SettingListener>();
            }
            listeners.add(l);
        }        
    }
    
	/* (non-Javadoc)
     * @see org.limewire.setting.Setting#removeSettingListener(org.limewire.setting.evt.SettingListener)
     */
    public void removeSettingListener(SettingListener l) {
        if (l == null) {
            throw new NullPointerException("SettingListener is null");
        }
        
        synchronized (this) {
            if (listeners != null) {
                listeners.remove(l);
                if (listeners.isEmpty()) {
                    listeners = null;
                }
            }
        }
    }
    
    /* (non-Javadoc)
     * @see org.limewire.setting.Setting#getSettingListeners()
     */
    public SettingListener[] getSettingListeners() {
        synchronized (this) {
            if (listeners == null) {
                return null;
            }
            
            return listeners.toArray(new SettingListener[0]);
        }
    }
    
    /* (non-Javadoc)
     * @see org.limewire.setting.Setting#reload()
     */
    public void reload() {
        String value = PROPS.getProperty(KEY);
        if (value == null) {
            value = DEFAULT_VALUE;
        }
        
        // Ensure that PROPS is always backed with the default value.
        // This is necessary for saving the PROPS file, as the backing
        // default map isn't serialized with the properties.
        // So any defaults that want to be "always saved" need to
        // be explicitly inserted.
        if(isDefault()) {
            PROPS.setProperty(KEY, DEFAULT_VALUE);
        }
        
        loadValue(value);
        fireSettingEvent(EventType.RELOAD);
    }

	/* (non-Javadoc)
     * @see org.limewire.setting.Setting#revertToDefault()
     */
	public boolean revertToDefault() {
	    if (!isDefault()) {
	        setValueInternal(DEFAULT_VALUE);
	        fireSettingEvent(EventType.REVERT_TO_DEFAULT);
	        return true;
	    }
	    return false;
	}
	
	/* (non-Javadoc)
     * @see org.limewire.setting.Setting#shouldAlwaysSave()
     */
    public boolean shouldAlwaysSave() {
        return alwaysSave;
    }
    
    /* (non-Javadoc)
     * @see org.limewire.setting.Setting#setAlwaysSave(boolean)
     */
    public AbstractSetting setAlwaysSave(boolean alwaysSave) {
        if (this.alwaysSave != alwaysSave) {
            this.alwaysSave = alwaysSave;
            fireSettingEvent(EventType.ALWAYS_SAVE_CHANGED);
        }
        return this;
    }
    
    /* (non-Javadoc)
     * @see org.limewire.setting.Setting#setPrivate(boolean)
     */
    public Setting setPrivate(boolean isPrivate) {
        if (this.isPrivate != isPrivate) {
            this.isPrivate = isPrivate;
            fireSettingEvent(EventType.PRIVACY_CHANGED);
        }
        return this;
    }
    
    /* (non-Javadoc)
     * @see org.limewire.setting.Setting#isPrivate()
     */
    public boolean isPrivate() {
        return isPrivate;
    }
	
    /* (non-Javadoc)
     * @see org.limewire.setting.Setting#isDefault()
     */
    public boolean isDefault() {
        String value = PROPS.getProperty(KEY);
        if (value == null)
            return true;
        return value.equals(DEFAULT_PROPS.getProperty(KEY));
    }
    
    /* (non-Javadoc)
     * @see org.limewire.setting.Setting#getKey()
     */
    public String getKey() {
        return KEY;
    }
    
    /* (non-Javadoc)
     * @see org.limewire.setting.Setting#getValueAsString()
     */
    public String getValueAsString() {
        String prop = PROPS.getProperty(KEY);
        return prop == null ? DEFAULT_VALUE : prop;
    }
    
    /**
     * Set new property value
     * @param value new property value
     * 
     * NOTE: This is protected so that only this package
     * can update all kinds of settings using a String value.
     * StringSetting updates the access to public.
     */
    protected void setValueInternal(String value) {
        String old = PROPS.getProperty(KEY);
        if (old == null || !old.equals(value)) {
            PROPS.setProperty(KEY, value);
            loadValue(value);
            fireSettingEvent(EventType.VALUE_CHANGED);
        }
    }

    /**
     * Load value from property string value
     * @param sValue property string value
     */
    abstract protected void loadValue(String sValue);    

    @Override
    public String toString() {
        return KEY + "=" + getValueAsString();
    }
    
    /**
     * Fires a SettingEvent
     */
    protected void fireSettingEvent(EventType type) {
        fireSettingEvent(new SettingEvent(type, this));
    }
    
    /**
     * Fires a SettingEvent
     */
    protected void fireSettingEvent(final SettingEvent evt) {
        if (evt == null) {
            throw new NullPointerException("SettingEvent is null");
        }
        
        final SettingListener[] listeners = getSettingListeners();
        if (listeners != null) {
            Runnable command = new Runnable() {
                public void run() {
                    for (SettingListener l : listeners) {
                        l.settingChanged(evt);
                    }
                }
            };
            
            SettingsGroupManager.instance().execute(command);
        }
    }
}
