/*
 * 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.meta;

import java.io.Serializable;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;

import org.apache.openjpa.conf.OpenJPAConfiguration;
import org.apache.openjpa.lib.log.Log;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.lib.util.StringDistance;
import org.apache.openjpa.lib.util.StringUtil;


/**
 * Vendor extensions. This class is thread safe for reads, but not for
 * mutations.
 *
 * @author Abe White
 */
public abstract class Extensions
    implements Serializable {

    
    private static final long serialVersionUID = 1L;

    public static final String OPENJPA = "openjpa";

    private static final Localizer _loc = Localizer.forPackage
        (Extensions.class);

    private Map _exts = null;
    private Map _embed = null;

    /**
     * Return true if there are no keys for any vendor.
     */
    public boolean isEmpty() {
        return (_exts == null || _exts.isEmpty())
            && (_embed == null || _embed.isEmpty());
    }

    /**
     * Return all vendors who have extension keys at this level.
     */
    public String[] getExtensionVendors() {
        if (_exts == null || _exts.isEmpty())
            return new String[0];

        Set vendors = new TreeSet();
        for (Object o : _exts.keySet()) {
            vendors.add(getVendor(o));
        }
        return (String[]) vendors.toArray(new String[vendors.size()]);
    }

    /**
     * Return all extension keys.
     */
    public String[] getExtensionKeys() {
        return getExtensionKeys(OPENJPA);
    }

    /**
     * Return all extension keys for the given vendor.
     */
    public String[] getExtensionKeys(String vendor) {
        if (_exts == null || _exts.isEmpty())
            return new String[0];

        Collection keys = new TreeSet();
        Object key;
        for (Object o : _exts.keySet()) {
            key = o;
            if (vendor.equals(getVendor(key)))
                keys.add(getKey(key));
        }
        return (String[]) keys.toArray(new String[keys.size()]);
    }

    /**
     * Return true if the extension with the given key exists.
     */
    public boolean hasExtension(String key) {
        return hasExtension(OPENJPA, key);
    }

    /**
     * Return true if the extension with the given key exists.
     */
    public boolean hasExtension(String vendor, String key) {
        return _exts != null && _exts.containsKey(getHashKey(vendor, key));
    }

    /**
     * Add a vendor extension to this entity.
     */
    public void addExtension(String key, Object value) {
        addExtension(OPENJPA, key, value);
    }

    /**
     * Add a vendor extension to this entity.
     */
    public void addExtension(String vendor, String key, Object value) {
        if (_exts == null)
            _exts = new HashMap();
        _exts.put(getHashKey(vendor, key), value);
    }

    /**
     * Remove a vendor extension.
     */
    public boolean removeExtension(String key) {
        return removeExtension(OPENJPA, key);
    }

    /**
     * Remove a vendor extension.
     */
    public boolean removeExtension(String vendor, String key) {
        if (_exts != null && _exts.remove(getHashKey(vendor, key)) != null) {
            removeEmbeddedExtensions(key);
            return true;
        }
        return false;
    }

    /**
     * Get the value of an extension.
     */
    public Object getObjectExtension(String key) {
        return getObjectExtension(OPENJPA, key);
    }

    /**
     * Get the value of an extension.
     */
    public Object getObjectExtension(String vendor, String key) {
        if (_exts == null)
            return null;
        return _exts.get(getHashKey(vendor, key));
    }

    /**
     * Get the value as a string.
     */
    public String getStringExtension(String key) {
        return getStringExtension(OPENJPA, key);
    }

    /**
     * Get the value as a string.
     */
    public String getStringExtension(String vendor, String key) {
        Object val = getObjectExtension(vendor, key);
        return (val == null) ? null : val.toString();
    }

    /**
     * Get the value as an int.
     */
    public int getIntExtension(String key) {
        return getIntExtension(OPENJPA, key);
    }

    /**
     * Get the value as an int.
     */
    public int getIntExtension(String vendor, String key) {
        String str = getStringExtension(vendor, key);
        return (str == null) ? 0 : Integer.parseInt(str);
    }

    /**
     * Get the value as a double.
     */
    public double getDoubleExtension(String key) {
        return getDoubleExtension(OPENJPA, key);
    }

    /**
     * Get the value as a double.
     */
    public double getDoubleExtension(String vendor, String key) {
        String str = getStringExtension(vendor, key);
        return (str == null) ? 0D : Double.parseDouble(str);
    }

    /**
     * Get the value as a boolean.
     */
    public boolean getBooleanExtension(String key) {
        return getBooleanExtension(OPENJPA, key);
    }

    /**
     * Get the value as a boolean.
     */
    public boolean getBooleanExtension(String vendor, String key) {
        String str = getStringExtension(vendor, key);
        return (str == null) ? false : Boolean.valueOf(str);
    }

    /**
     * Return the embedded extensions under the given key.
     */
    public Extensions getEmbeddedExtensions(String key, boolean create) {
        return getEmbeddedExtensions(OPENJPA, key, create);
    }

    /**
     * Return the embedded extensions under the given key.
     */
    public Extensions getEmbeddedExtensions(String vendor, String key,
        boolean create) {
        if (_embed == null && !create)
            return null;
        if (_embed == null)
            _embed = new HashMap();

        Object hk = getHashKey(vendor, key);
        Extensions exts = (Extensions) _embed.get(hk);
        if (exts == null && !create)
            return null;
        if (exts == null) {
            exts = new EmbeddedExtensions(this);
            _embed.put(hk, exts);

            // required to recognize embedded extensions without values
            if (_exts == null)
                _exts = new HashMap();
            if (!_exts.containsKey(hk))
                _exts.put(hk, null);
        }
        return exts;
    }

    public boolean removeEmbeddedExtensions(String key) {
        return removeEmbeddedExtensions(OPENJPA, key);
    }

    public boolean removeEmbeddedExtensions(String vendor, String key) {
        return _embed != null
            && _embed.remove(getHashKey(vendor, key)) != null;
    }

    /**
     * Copy the extensions not present in this instance but present in the
     * given instance.
     */
    protected void copy(Extensions exts) {
        if (exts.isEmpty())
            return;

        if (exts._exts != null && !exts._exts.isEmpty()) {
            if (_exts == null)
                _exts = new HashMap();

            Map.Entry entry;
            for (Object o : exts._exts.entrySet()) {
                entry = (Map.Entry) o;
                if (!_exts.containsKey(entry.getKey()))
                    _exts.put(entry.getKey(), entry.getValue());
            }
        }

        if (exts._embed != null && !exts._embed.isEmpty()) {
            if (_embed == null)
                _embed = new HashMap();

            Map.Entry entry;
            Extensions embedded;
            for (Object o : exts._embed.entrySet()) {
                entry = (Map.Entry) o;
                embedded = (Extensions) _embed.get(entry.getKey());
                if (embedded == null) {
                    embedded = new EmbeddedExtensions(this);
                    _embed.put(entry.getKey(), embedded);
                }
                embedded.copy((Extensions) entry.getValue());
            }
        }
    }

    /**
     * Helper method to issue warnings for any extensions that we
     * recognize but do not use.
     *
     * @since 0.3.1.3
     */
    public void validateExtensionKeys() {
        if (_exts == null || _exts.isEmpty())
            return;

        OpenJPAConfiguration conf = getRepository().getConfiguration();
        Log log = conf.getLog(OpenJPAConfiguration.LOG_METADATA);
        if (!log.isWarnEnabled())
            return;

        Collection validNames = new TreeSet();
        addExtensionKeys(validNames);

        // this is where we store things like "jdbc-" for a
        // prefix for an extension name that we won't validate; that
        // way a new vendor could theoretically add in their
        // own prefix into the localizer.properties file and
        // not have to issue warnings for their extensions
        String prefixes = _loc.get("extension-datastore-prefix").getMessage();
        String[] allowedPrefixes = null;
        if (prefixes != null)
            allowedPrefixes = StringUtil.split(prefixes, ",", 0);

        Object next;
        String key;
        outer:
        for (Object o : _exts.keySet()) {
            next = o;
            if (!OPENJPA.equals(getVendor(next)))
                continue;
            key = getKey(next);
            if (validNames.contains(key))
                continue;

            if (allowedPrefixes != null) {
                for (String allowedPrefix : allowedPrefixes) {
                    if (key.startsWith(allowedPrefix)
                            && !validateDataStoreExtensionPrefix
                            (allowedPrefix))
                        continue outer;
                }
            }

            // try to determine if there are any other names that are
            // similiar to this one, so we can add in a hint
            String closestName = StringDistance.getClosestLevenshteinDistance
                    (key, validNames, 0.5f);

            if (closestName == null)
                log.warn(_loc.get("unrecognized-extension", this,
                        key, validNames));
            else
                log.warn(_loc.get("unrecognized-extension-hint",
                        new Object[]{this, key, validNames, closestName}));
        }
    }

    /**
     * Add all the known extension keys to the specified collection; any
     * implementation that utilized new extensions should override this
     * method to include both the known extensions of its superclass as well
     * as its own extension keys.
     *
     * @since 0.3.1.3
     */
    protected void addExtensionKeys(Collection exts) {
        // no extensions by default
    }

    /**
     * Return true if extensions starting with the given official datastore
     * prefix should be validated for this runtime.
     */
    protected boolean validateDataStoreExtensionPrefix(String prefix) {
        return false;
    }

    /**
     * Return the metadata repository.
     */
    public abstract MetaDataRepository getRepository();

    /**
     * Create a hash key for the given vendor/key combo.
     */
    private Object getHashKey(String vendor, String key) {
        if (OPENJPA.equals(vendor))
            return key;
        return new HashKey(vendor, key);
    }

    /**
     * Extract the vendor from the given hash key.
     */
    private String getVendor(Object hashKey) {
        return (hashKey instanceof String) ? OPENJPA :
            ((HashKey) hashKey).vendor;
    }

    /**
     * Extract the key from the given hash key.
     */
    private String getKey(Object hashKey) {
        return (hashKey instanceof String) ? (String) hashKey
            : ((HashKey) hashKey).key;
    }

    /**
     * Key class.
     */
    private static class HashKey
        implements Serializable {

        
        private static final long serialVersionUID = 1L;
        public final String vendor;
        public final String key;

        public HashKey(String vendor, String key) {
            this.vendor = vendor;
            this.key = key;
        }

        @Override
        public int hashCode() {
            int i = 0;
            if (vendor != null)
                i = vendor.hashCode();
            if (key != null)
                i += 17 * key.hashCode();
            return i;
        }

        @Override
        public boolean equals(Object other) {
            if (other == this)
                return true;
            HashKey hk = (HashKey) other;
            return Objects.equals(vendor, hk.vendor)
                && Objects.equals(key, hk.key);
        }
    }

    /**
     * Embedded extensions implementation.
     */
    private static class EmbeddedExtensions
        extends Extensions {

        
        private static final long serialVersionUID = 1L;
        private final Extensions _parent;

        public EmbeddedExtensions(Extensions parent) {
            _parent = parent;
        }

        @Override
        public MetaDataRepository getRepository ()
		{
			return _parent.getRepository ();
		}
	}
}
