/*
 * 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.brooklyn.core.config.internal;

import java.util.Map;
import java.util.concurrent.ExecutionException;

import org.apache.brooklyn.api.mgmt.ExecutionContext;
import org.apache.brooklyn.config.ConfigKey;
import org.apache.brooklyn.core.config.BasicConfigKey;
import org.apache.brooklyn.core.config.StructuredConfigKey;
import org.apache.brooklyn.core.config.SubElementConfigKey;
import org.apache.brooklyn.util.exceptions.Exceptions;

import com.google.common.collect.Maps;
import com.google.common.reflect.TypeToken;

public abstract class AbstractStructuredConfigKey<T,RawT,V> extends BasicConfigKey<T> implements StructuredConfigKey {

    private static final long serialVersionUID = 7806267541029428561L;

    private final TypeToken<V> subTypeToken;
    private final Class<V> subType;

    @SuppressWarnings("unchecked")
    protected AbstractStructuredConfigKey(BasicConfigKey.Builder<T,?> builder, TypeToken<V> subType) {
        super(builder);
        if (TypeToken.of(subType.getRawType()).equals(subType)) {
            // not generic; store raw type
            this.subType = (Class<V>) subType.getRawType();
            this.subTypeToken = null;
        } else {
            // store generic type token
            this.subTypeToken = subType;
            this.subType = null;
        }
    }

    public AbstractStructuredConfigKey(TypeToken<T> type, TypeToken<V> subType, String name, String description, T defaultValue) {
        super(type, name, description, defaultValue);
        this.subTypeToken = subType;
        this.subType = null;
    }

    protected ConfigKey<V> subKey(String subName) {
        return subKey(subName, "sub-element of " + getName() + ", named " + subName);
    }
    // it is not possible to supply default values
    protected ConfigKey<V> subKey(String subName, String description) {
        return new SubElementConfigKey<V>(this, getSubTypeToken(), getName() + "." + subName, description, null);
    }
    
    protected TypeToken<V> getSubTypeToken() {
        if (subTypeToken!=null) return subTypeToken;
        if (subType!=null) return TypeToken.of(subType);
        return null;
    }

    protected static String getKeyName(Object contender) {
        if (contender==null) return null;
        if (contender instanceof ConfigKey) return ((ConfigKey<?>)contender).getName();
        return contender.toString();
    }
    
    @Override
    public boolean acceptsKeyMatch(Object contender) {
        return (getName().equalsIgnoreCase(getKeyName(contender)));
    }
    
    @Override
    public boolean acceptsSubkey(Object contender) {
        return contender!=null && getKeyName(contender).startsWith(getName()+".");        
    }
    
    public String extractSubKeyName(Object o) {
        String name = getKeyName(o);
        assert name.startsWith(getName()+".");
        return name.substring(getName().length() + 1);
    }

    @Override
    public boolean acceptsSubkeyStronglyTyped(Object contender) {
        return (contender instanceof SubElementConfigKey) && 
            acceptsKeyMatch( ((SubElementConfigKey<?>) contender).parent );
    }
    
    @Override
    public boolean isSet(Map<?, ?> vals) {
        if (vals.containsKey(this))
            return true;
        for (Object contender : vals.keySet()) {
            if (acceptsKeyMatch(contender) || acceptsSubkey(contender)) {
                return true;
            }
        }
        return false;
    }

    protected RawT extractValue(Map<?,?> vals, ExecutionContext exec, boolean coerce, boolean unmodifiable) {
        RawT base = null; 
        Map<String,Object> subkeys = Maps.newLinkedHashMap();
        for (Map.Entry<?,?> entry : vals.entrySet()) {
            Object k = entry.getKey();
            // we don't resolve the key above because this map is the root map;
            // deferred values as keys must be at an explicit config key entry

            if (acceptsKeyMatch(k)) {
                try {
                    base = extractValueMatchingThisKey(entry.getValue(), exec, coerce);
                } catch (Exception e) { throw Exceptions.propagate(e); }
            }

            if (acceptsSubkey(k)) {
                String subKeyName = extractSubKeyName(k);
                Object value;
                if (coerce) {
                    @SuppressWarnings("unchecked")
                    SubElementConfigKey<V> kk = k instanceof SubElementConfigKey<?> ?
                        (SubElementConfigKey<V>) k : (SubElementConfigKey<V>) subKey(subKeyName);
                    value = kk.extractValue(vals, exec);
                } else {
                    value = vals.get(k);
                }
                subkeys.put(subKeyName, value);
            }
        }
        return merge(base, subkeys, unmodifiable);
    }

    @SuppressWarnings("unchecked")
    @Override
    public T extractValue(Map<?,?> vals, ExecutionContext exec) {
        return (T) extractValue(vals, exec, true, true);
    }

    /** returns the entries in the map against this config key and any sub-config-keys, without resolving
     * (like {@link #extractValue(Map, ExecutionContext)} but without resolving/coercing;
     * useful because values in this "map" are actually stored against {@link SubElementConfigKey}s */
    public RawT rawValue(Map<?,?> vals) {
        return extractValue(vals, null, false, false);
    }

    /** returns value against *this* key, if it is of an acceptable type (ignoring subkeys which are added on top) */
    protected abstract RawT extractValueMatchingThisKey(Object potentialBase, ExecutionContext exec, boolean coerce) throws InterruptedException, ExecutionException;
    
    protected abstract RawT merge(RawT base, Map<String, Object> subkeys, boolean unmodifiable);
    
}
