package edu.caltech.nanodb.server.properties;


import java.util.ArrayList;
import java.util.Collections;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import edu.caltech.nanodb.storage.DBFile;


/**
 * This is the central location where all properties exposed by the database
 * can be registered and accessed.  Various components register their
 * configurable properties with the registry, and then they can be accessed
 * and/or set from the SQL prompt.
 */
public class PropertyRegistry implements ServerProperties {

    private class PropertyDescriptor {
        /** The name of the property. */
        String name;

        /** The current value of the property. */
        Object value;

        /**
         * A flag indicating whether the property is read-only or read-write.
         */
        boolean readonly;

        /** A validator to ensure the property's values are correct. */
        PropertyValidator validator;


        PropertyDescriptor(String name, PropertyValidator validator,
                           Object initialValue, boolean readonly) {
            this.name = name;
            this.validator = validator;

            // Set readonly to false for initial write.
            this.readonly = false;
            setValue(initialValue);

            // Now, set readonly flag to what it should be.
            this.readonly = readonly;
        }


        public Object getValue() {
            return value;
        }


        public void setValue(Object newValue) {
            if (!setup && readonly) {
                throw new PropertyException("Property \"" + name +
                    "\" is read-only during normal operation, and should " +
                    "only be set at start-up.");
            }

            value = validator.validate(newValue);
        }
    }


    /**
     * A mapping of property names to values.  A thread-safe hash map is used
     * since this will be accessed and mutated from different threads.
     */
    private ConcurrentHashMap<String, PropertyDescriptor> properties =
        new ConcurrentHashMap<>();


    /**
     * A collection of property observers to be informed when property values
     * change.
     */
    private ArrayList<PropertyObserver> observers = new ArrayList<>();


    /**
     * This flag indicates whether the database is still in "start-up mode"
     * or not.  During start-up mode, all read-only properties may also be
     * modified.
     */
    private boolean setup = true;


    public PropertyRegistry() {
        initProperties();
    }


    /**
     * Initialize all of the properties that the database server knows about.
     * Some of these properties are read-write, and others are read-only and
     * are initialized right at database startup, before any other steps
     * occur.
     *
     * @review (donnie) It's not great that this configuration is in here.
     *         Someday, should probably migrate it back into
     *         {@code NanoDBServer}.
     */
    private void initProperties() {
        addProperty(PROP_BASE_DIRECTORY,
            new StringValueValidator(), DEFAULT_BASE_DIRECTORY,
            /* readonly */ true);

        addProperty(PROP_PAGECACHE_SIZE,
            new IntegerValueValidator(MIN_PAGECACHE_SIZE, MAX_PAGECACHE_SIZE),
            DEFAULT_PAGECACHE_SIZE);

        addProperty(PROP_PAGECACHE_POLICY,
            new StringEnumValidator(PAGECACHE_POLICY_VALUES),
            DEFAULT_PAGECACHE_POLICY, /* readonly */ true);

        addProperty(PROP_PAGE_SIZE,
            new IntegerValueValidator(DBFile::isValidPageSize,
                "Specified page-size %d is invalid."), DEFAULT_PAGE_SIZE);

        addProperty(PROP_ENABLE_TRANSACTIONS,
            new BooleanFlagValidator(), false, /* readonly */ true);

        addProperty(PROP_ENFORCE_KEY_CONSTRAINTS,
            new BooleanFlagValidator(), true);

        addProperty(PROP_ENABLE_INDEXES,
            new BooleanFlagValidator(), false, /* reaadonly */ true);

        addProperty(PROP_CREATE_INDEXES_ON_KEYS,
            new BooleanFlagValidator(), false);

        addProperty(PROP_PLANNER_CLASS,
            new PlannerClassValidator(), DEFAULT_PLANNER_CLASS);

        addProperty(PROP_FLUSH_AFTER_CMD, new BooleanFlagValidator(), true);
    }


    /**
     * This helper function sets the server properties based on the contents
     * of a Java {@code Properties} object.  This allows us to set NanoDB
     * properties from system properties and/or other sources of properties.
     *
     * @param properties the properties to apply to NanoDB's configuration.
     */
    public void setProperties(Properties properties) {
        if (properties == null)
            throw new IllegalArgumentException("properties cannot be null");

        for (String name : properties.stringPropertyNames()) {
            if (hasProperty(name))
                setPropertyValue(name, properties.getProperty(name));
        }
    }


    /**
     * Records that setup has been completed, and read-only properties
     * should no longer be allowed to change.
     */
    public void setupCompleted() {
        setup = false;
    }


    /**
     * Records a property-change observer on the property registry.
     *
     * @param observer the observer to receive property-change notifications
     */
    public void addObserver(PropertyObserver observer) {
        if (observer == null)
            throw new IllegalArgumentException("observer cannot be null");

        observers.add(observer);
    }


    /**
     * Add a read-only or read-write property to the registry, along with a
     * type and an initial value.
     *
     * @param name the name of the property
     * @param validator a validator for the property
     * @param initialValue an initial value for the property
     * @param readonly a flag indicating whether the property is read-only
     *        ({@code true}) or read-write ({@code false})
     */
    public void addProperty(String name, PropertyValidator validator,
                            Object initialValue, boolean readonly) {
        properties.put(name,
            new PropertyDescriptor(name, validator, initialValue, readonly));
    }


    /**
     * Add a read-write property to the registry, along with a
     * type and an initial value.
     *
     * @param name the name of the property
     * @param validator a validator for the property
     * @param initialValue an initial value for the property
     */
    public void addProperty(String name, PropertyValidator validator,
                            Object initialValue) {
        addProperty(name, validator, initialValue, false);
    }


    /**
     * Returns {@code true} if the server has a property of the specified
     * name, {@code false} otherwise.
     *
     * @param name the non-null name of the property
     * @return {@code true} if the server has a property of the specified
     *         name, {@code false} otherwise.
     */
    public boolean hasProperty(String name) {
        if (name == null)
            throw new IllegalArgumentException("name cannot be null");

        return properties.containsKey(name);
    }


    /**
     * Returns an unmodifiable set of all property names.
     *
     * @return an unmodifiable set of all property names.
     */
    public Set<String> getAllPropertyNames() {
        return Collections.unmodifiableSet(properties.keySet());
    }


    public Object getPropertyValue(String name)
        throws PropertyException {

        if (name == null)
            throw new IllegalArgumentException("name cannot be null");

        if (!properties.containsKey(name)) {
            throw new PropertyException("No property named \"" +
                name + "\"");
        }

        return properties.get(name).getValue();
    }


    public void setPropertyValue(String name, Object value) {
        if (name == null)
            throw new IllegalArgumentException("name cannot be null");

        if (!properties.containsKey(name)) {
            throw new PropertyException("No property named \"" +
                name + "\"");
        }

        properties.get(name).setValue(value);

        for (PropertyObserver obs : observers)
            obs.propertyChanged(name, value);
    }


    /**
     * Returns a property's value as a Boolean.  If the property's value is
     * not a Boolean then an exception is reported.
     *
     * @param name the name of the property to fetch
     *
     * @return a Boolean true or false value for the property
     */
    public boolean getBooleanProperty(String name) {
        return (Boolean) getPropertyValue(name);
    }


    /**
     * Returns a property's value as a String.  If the property's value is
     * not a String then an exception is reported.
     *
     * @param name the name of the property to fetch
     *
     * @return a String value for the property
     */
    public String getStringProperty(String name) {
        return (String) getPropertyValue(name);
    }


    /**
     * Returns a property's value as an integer.  If the property's value is
     * not an integer then an exception is reported.
     *
     * @param name the name of the property to fetch
     *
     * @return an integer value for the property
     */
    public int getIntProperty(String name) {
        return (Integer) getPropertyValue(name);
    }
}