/*
 * 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.openjpa.lib.conf;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.lib.util.ParseException;

/**
 * A configuration value.
 *
 * @author Marc Prud'hommeaux
 * @author Pinaki Poddar
 */
public abstract class Value implements Cloneable {

    private static final String[] EMPTY_ALIASES = new String[0];
    private static final Localizer s_loc = Localizer.forPackage(Value.class);
    public static final String INVISIBLE = "******";

    private String prop = null;
    private String loadKey = null;
    private String def = null;
    private String[] aliases = null;
    private String getter = null;
    private List<ValueListener> listeners = null;
    private boolean aliasListComprehensive = false;
    private Class scope = null;
    private boolean isDynamic = false;
    private String originalValue = null;
    private Set<String> otherNames = null;
    private boolean _hidden  = false;
    private boolean _private = false;

    /**
     * Default constructor.
     */
    public Value() {
    }

    /**
     * Constructor. Supply the property name.
     *
     * @see #setProperty
     */
    public Value(String prop) {
        setProperty(prop);
    }

    /**
     * The property name that will be used when setting or
     * getting this value in a {@link Map}.
     */
    public String getProperty() {
        return prop;
    }

    /**
     * The property name that will be used when setting or
     * getting this value in a {@link Map}.
     */
    public void setProperty(String prop) {
        this.prop = prop;
    }

    /**
     * Adds a moniker that is equivalent to the original property key used
     * during construction.
     *
     * @since 2.0.0
     */
    public void addEquivalentKey(String other) {
        if (otherNames == null)
            otherNames = new HashSet<>();
        otherNames.add(other);
    }

    /**
     * Gets the unmodifiable view of the equivalent keys or an empty set if
     * no equivalent key has been added.
     *
     * @since 2.0.0
     */
    public Set<String> getEquivalentKeys() {
        return otherNames == null ? Collections.EMPTY_SET
            : Collections.unmodifiableSet(otherNames);
    }

    /**
     * Gets unmodifiable view of all the property keys set on this receiver.
     * The 0-th element in the returned list is always the same as the original
     * key returned by {@link #getProperty()} method.
     *
     * @since 2.0.0
     */
    public List<String> getPropertyKeys() {
        List<String> result = new ArrayList<>(1 +
            (otherNames ==null ? 0 : otherNames.size()));
        result.add(getProperty());
        if (otherNames != null)
            result.addAll(otherNames);
        return Collections.unmodifiableList(result);
    }

    /**
     * Affirms if the given key matches the property (or any of its equivalent).
     *
     * @since 2.0.0
     */
    public boolean matches(String p) {
        return getProperty().equals(p) ||
          (otherNames != null && otherNames.contains(p));
    }

    /**
     * The key under which this value was loaded, or null.
     */
    public String getLoadKey() {
        return loadKey;
    }

    /**
     * Sets key under which this value was loaded.
     * @exception if called with a non-null key which is different from an
     * already loaded key.
     */
    public void setLoadKey(String key) {
        if (this.loadKey != null && key != null && !this.loadKey.equals(key))
            throw new ParseException(s_loc.get("multiple-load-key",
                loadKey, key));
        loadKey = key;
    }

    /**
     * Aliases for the value in the form key1, value1, key2, value2, ...
     * All alias values must be in string form.
     */
    public String[] getAliases() {
        return (aliases == null) ? EMPTY_ALIASES : aliases;
    }

    /**
     * Aliases for the value in the form key1, value1, key2, value2, ...
     * All alias values must be in string form.
     * <p>
     * To avoid potential side-effects, this method copies the array passed in.
     */
    public void setAliases(String[] aliases) {
        String [] aStrings = new String[aliases.length];
        System.arraycopy(aliases, 0, aStrings, 0, aStrings.length);
        this.aliases = aStrings;
    }

    /**
     * Replaces an existing alias, or adds the given alias to the front of the
     * alias list if it does not already exist. All alias values must be in
     * string form.
     */
    public void setAlias(String key, String value) {
        aliases = setAlias(key, value, aliases);
    }

    /**
     * Set an alias into a current alias list, returning the new list.
     */
    protected String[] setAlias(String key, String value, String[] aliases) {
        if (aliases == null)
            aliases = EMPTY_ALIASES;
        for (int i = 0; i < aliases.length; i += 2) {
            if (key.equals(aliases[i])) {
                aliases[i + 1] = value;
                return aliases;
            }
        }

        // add as new alias
        String[] newAliases = new String[aliases.length + 2];
        System.arraycopy(aliases, 0, newAliases, 2, aliases.length);
        newAliases[0] = key;
        newAliases[1] = value;
        return newAliases;
    }

    /**
     * Whether or not the alias list defines all possible settings for this
     * value. If so, an error will be generated when attempting to invoke
     * any method on this value with an unknown option.
     */
    public boolean isAliasListComprehensive() {
        return aliasListComprehensive;
    }

    /**
     * Whether or not the alias list defines all possible settings for this
     * value. If so, an error will be generated when attempting to invoke
     * any method on this value with an unknown option.
     */
    public void setAliasListComprehensive(boolean aliasListIsComprehensive) {
        this.aliasListComprehensive = aliasListIsComprehensive;
    }

    /**
     * Alias the given setting.
     */
    public String alias(String str) {
        return alias(str, aliases, false);
    }

    /**
     * Alias the given setting.
     */
    protected String alias(String str, String[] aliases, boolean nullNotFound) {
        if (str != null)
            str = str.trim();
        if (aliases == null || aliases.length == 0)
            return (nullNotFound) ? null : str;

        boolean empty = str != null && str.length() == 0;
        for (int i = 1; i < aliases.length; i += 2)
            if (Objects.equals(str, aliases[i])
                || (empty && aliases[i] == null))
                return aliases[i - 1];
        return (nullNotFound) ? null : str;
    }

    /**
     * Unalias the given setting.
     */
    public String unalias(String str) {
        return unalias(str, aliases, false);
    }

    /**
     * Unalias the given setting.
     */
    protected String unalias(String str, String[] aliases,
        boolean nullNotFound) {
        if (str != null)
            str = str.trim();

        boolean empty = str != null && str.length() == 0;
        if (str == null || (empty && def != null))
            str = def;
        if (aliases != null)
            for (int i = 0; i < aliases.length; i += 2)
                if (Objects.equals(str, aliases[i])
                    || Objects.equals(str, aliases[i + 1])
                    || (empty && aliases[i] == null))
                    return aliases[i + 1];

        if (isAliasListComprehensive() && aliases != null)
            throw new ParseException(s_loc.get("invalid-enumerated-config",
                getProperty(), str, Arrays.asList(aliases)));

        return (nullNotFound) ? null : str;
    }

    /**
     * The default value for the property as a string.
     */
    public String getDefault() {
        return def;
    }

    /**
     * The default value for the property as a string.
     */
    public void setDefault(String def) {
        this.def = def;
    }

    /**
     * The name of the getter method for the instantiated value of this
     * property(as opposed to the string value)
     */
    public String getInstantiatingGetter() {
        return getter;
    }

    /**
     * The name of the getter method for the instantiated value of this
     * property(as opposed to the string value). If the string starts with
     * <code>this.</code>, then the getter will be looked up on the value
     * instance itself. Otherwise, the getter will be looked up on the
     * configuration instance.
     */
    public void setInstantiatingGetter(String getter) {
        this.getter = getter;
    }

    /**
     * A class defining the scope in which this value is defined. This will
     * be used by the configuration framework to look up metadata about
     * the value.
     */
    public Class getScope() {
        return scope;
    }

    /**
     * A class defining the scope in which this value is defined. This will
     * be used by the configuration framework to look up metadata about
     * the value.
     */
    public void setScope(Class cls) {
        scope = cls;
    }

    /**
     * Return a stringified version of this value. If the current value has
     * a short alias key, the alias key is returned.
     */
    public String getString() {
        return alias(getInternalString());
    }

    /**
     * Set this value from the given string. If the given string is null or
     * empty and a default is defined, the default is used. If the given
     * string(or default) is an alias key, it will be converted to the
     * corresponding value internally.
     * <br>
     * If this Value is being set to a non-default value for the first time
     * (as designated by <code>originalString</code> being null), then the
     * value is remembered as <em>original</em>. This original value is used
     * for equality and hashCode computation if this Value is
     * {@link #isDynamic() dynamic}.
     *
     */
    public void setString(String val) {
    	assertChangeable();
        String str = unalias(val);
        try {
            setInternalString(str);
            if (originalValue == null && val != null && !isDefault(val)) {
            	originalValue = getString();
            }
        } catch (ParseException pe) {
            throw pe;
        } catch (RuntimeException re) {
            throw new ParseException(prop + ": " + val, re);
        }
    }

    /**
     * Set this value as an object.
     * <br>
     * If this Value is being set to a non-default value for the first time
     * (as designated by <code>originalString</code> being null), then the
     * value is remembered as <em>original</em>. This original value is used
     * for equality and hashCode computation if this Value is
     * {@link #isDynamic() dynamic}.
     *
     */
    public void setObject(Object obj) {
        // if setting to null set as string to get defaults into play
        if (obj == null && def != null)
            setString(null);
        else {
            try {
                setInternalObject(obj);
                if (originalValue == null && obj != null && !isDefault(obj)) {
                	originalValue = getString();
                }
            } catch (ParseException pe) {
                throw pe;
            } catch (RuntimeException re) {
                throw new ParseException(prop + ": " + obj, re);
            }
        }
    }

    /**
     * Gets the original value. Original value denotes the Stringified form of
     * this Value, from which it has been set, if ever. If this Value has never
     * been set to a non-default value, then returns the default value, which
     * itself can be null.
     *
     * @since 1.1.0
     */
    public String getOriginalValue() {
    	return (originalValue == null) ? getDefault() : originalValue;
    }

    boolean isDefault(Object val) {
    	return val != null && val.toString().equals(getDefault());
    }

    /**
     * Returns the type of the property that this Value represents.
     */
    public abstract Class<?> getValueType();

    /**
     * Return the internal string form of this value.
     */
    protected abstract String getInternalString();

    /**
     * Set this value from the given string.
     */
    protected abstract void setInternalString(String str);

    /**
     * Set this value from an object.
     */
    protected abstract void setInternalObject(Object obj);

    /**
     * Gets unmodifable list of listeners for value changes.
     */
    public List<ValueListener> getListeners() {
        return Collections.unmodifiableList(this.listeners);
    }

    /**
     * Listener for value changes.
     */
    public void addListener(ValueListener listener) {
    	if (listener == null)
    		return;
    	if (listeners == null)
    		listeners = new ArrayList<>();
        listeners.add(listener);
    }

    public void removeListener(ValueListener listener) {
    	if (listener == null)
    		return;
        listeners.remove(listener);
    }

    /**
     * Subclasses should call this method when their internal value changes.
     */
    public void valueChanged() {
        if (listeners == null)
        	return;
        for (ValueListener listener : listeners) {
        	listener.valueChanged(this);
        }
    }

    /**
     * Asserts if this receiver can be changed.
     * Subclasses <em>must</em> invoke this method before changing its
     * internal state.
     *
     * This receiver can not be changed if all of the following is true
     * <LI>this receiver is not dynamic
     * <LI>ValueListener attached to this receiver is a Configuration
     * <LI>Configuration is read-only
     */
    protected void assertChangeable() {
    	if (!isDynamic() && containsReadOnlyConfigurationAsListener()) {
        	throw new RuntimeException(s_loc.get("veto-change",
        		this.getProperty()).toString());
       	}
    }

    boolean containsReadOnlyConfigurationAsListener() {
    	if (listeners == null)
    		return false;
    	for (ValueListener listener : listeners) {
    		if (listener instanceof Configuration
    		&& ((Configuration)listener).isReadOnly())
    			return true;
    	}
    	return false;
    }

    /**
     * Sets if this receiver can be mutated even when the configuration it
     * belongs to has been {@link Configuration#isReadOnly() frozen}.
     *
     * @since 1.1.0
     */
    public void setDynamic(boolean flag) {
    	isDynamic = flag;
    }

    /**
     * Affirms if this receiver can be mutated even when the configuration it
     * belongs to has been {@link Configuration#isReadOnly() frozen}.
     *
     * @since 1.1.0
     */
    public boolean isDynamic() {
    	return isDynamic;
    }

    /**
     * Use {@link #getOriginalValue() original value} instead of
     * {@link #getString() current value} because they are one and the same
     * for non-dynamic Values and ensures that modifying dynamic Values do not
     * impact equality or hashCode contract.
     */
    @Override
    public int hashCode() {
        String str = (isDynamic()) ? getOriginalValue() : getString();
        int strHash = (str == null) ? 0 : str.hashCode();
        int propHash = (prop == null) ? 0 : prop.hashCode();
        return strHash ^ propHash;
    }

    /**
     * Use {@link #getOriginalValue() original value} instead of
     * {@link #getString() current value} because they are one and the same
     * for non-dynamic Values and ensures that modifying dynamic Values do not
     * impact equality or hashCode contract.
     */
    @Override
    public boolean equals(Object other) {
        if (other == this)
            return true;
        if (!(other instanceof Value))
            return false;

        Value o = (Value) other;
        String thisStr = (isDynamic()) ? getOriginalValue() : getString();
        String thatStr = (isDynamic()) ? o.getOriginalValue() : o.getString();
        return (isDynamic() == o.isDynamic())
            && Objects.equals(prop, o.getProperty())
            && Objects.equals(thisStr, thatStr);
    }

    @Override
    public Object clone() {
        try {
            return super.clone();
        } catch (CloneNotSupportedException cnse) {
            return null;
        }
    }

    /**
     * Affirms if the value for this Value is visible.
     * Certain sensitive value such as password can be made invisible
     * so that it is not returned to the user code.
     */
    public boolean isHidden() {
        return _hidden;
    }

    /**
     * Hides the value of this Value from being output to the caller.
     */
    public void hide() {
        _hidden = true;
    }

    /**
     * Affirms if this Value is used for internal purpose only and not exposed as a supported property.
     * @see Configuration#getPropertyKeys()
     */
    public boolean isPrivate() {
        return _private;
    }

    /**
     * Marks this Value for internal purpose only.
     */
    public void makePrivate() {
        _private = true;
    }

    /**
     * Get the actual data stored in this value.
     */
    public abstract Object get();

    @Override
    public String toString() {
        return getProperty()+ ":" + get() + "[" + getValueType().getName() + "]";
    }
}
