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

import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Collections;
import java.util.Map;
import java.util.Set;

import org.apache.brooklyn.api.objs.EntityAdjunct;
import org.apache.brooklyn.config.ConfigKey;
import org.apache.brooklyn.config.ConfigKey.HasConfigKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Joiner;
import com.google.common.base.MoreObjects;
import com.google.common.base.Objects;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;

/**
 * This is the actual type of a policy instance at runtime.
 */
public class AdjunctType implements Serializable {
    private static final long serialVersionUID = -662979234559595903L;

    private static final Logger LOG = LoggerFactory.getLogger(AdjunctType.class);

    private final String name;
    private final Map<String, ConfigKey<?>> configKeys;
    private final Set<ConfigKey<?>> configKeysSet;

    public AdjunctType(AbstractEntityAdjunct adjunct) {
        this(adjunct.getClass(), adjunct);
    }
    
    protected AdjunctType(Class<? extends EntityAdjunct> clazz) {
        this(clazz, null);
    }
    
    private AdjunctType(Class<? extends EntityAdjunct> clazz, AbstractEntityAdjunct adjunct) {
        name = clazz.getCanonicalName();
        configKeys = Collections.unmodifiableMap(findConfigKeys(clazz, null));
        configKeysSet = ImmutableSet.copyOf(this.configKeys.values());
        if (LOG.isTraceEnabled())
            LOG.trace("Policy {} config keys: {}", name, Joiner.on(", ").join(configKeys.keySet()));
    }
    
    AdjunctType(String name, Map<String, ConfigKey<?>> configKeys) {
        this.name = name;
        this.configKeys = ImmutableMap.copyOf(configKeys);
        this.configKeysSet = ImmutableSet.copyOf(this.configKeys.values());
    }

    public String getName() {
        return name;
    }
    
    public Set<ConfigKey<?>> getConfigKeys() {
        return configKeysSet;
    }
    
    public ConfigKey<?> getConfigKey(String name) {
        return configKeys.get(name);
    }
    
    @Override
    public int hashCode() {
        return Objects.hashCode(name, configKeys);
    }
    
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (getClass() != obj.getClass()) return false;
        AdjunctType o = (AdjunctType) obj;
        if (!Objects.equal(name, o.getName())) return false;
        if (!Objects.equal(getConfigKeys(), o.getConfigKeys())) return false;
        return true;
    }
    
    @Override
    public String toString() {
        return MoreObjects.toStringHelper(name)
                .add("configKeys", configKeys)
                .toString();
    }
    
    /**
     * Finds the config keys defined on the entity's class, statics and optionally any non-static (discouraged).
     */
    // TODO Remove duplication from EntityDynamicType
    protected static Map<String,ConfigKey<?>> findConfigKeys(Class<? extends EntityAdjunct> clazz, EntityAdjunct optionalInstance) {
        try {
            Map<String,ConfigKey<?>> result = Maps.newLinkedHashMap();
            Map<String,Field> configFields = Maps.newLinkedHashMap();
            for (Field f : clazz.getFields()) {
                boolean isConfigKey = ConfigKey.class.isAssignableFrom(f.getType());
                if (!isConfigKey) {
                    if (!HasConfigKey.class.isAssignableFrom(f.getType())) {
                        // neither ConfigKey nor HasConfigKey
                        continue;
                    }
                }
                if (!Modifier.isStatic(f.getModifiers())) {
                    // require it to be static or we have an instance
                    LOG.warn("Discouraged use of non-static config key "+f+" defined in " + (optionalInstance!=null ? optionalInstance : clazz));
                    if (optionalInstance==null) continue;
                }
                ConfigKey<?> k = isConfigKey ? (ConfigKey<?>) f.get(optionalInstance) : 
                    ((HasConfigKey<?>)f.get(optionalInstance)).getConfigKey();

                Field alternativeField = configFields.get(k.getName());
                // Allow overriding config keys (e.g. to set default values) when there is an assignable-from relationship between classes
                Field definitiveField = alternativeField != null ? inferSubbestField(alternativeField, f) : f;
                boolean skip = false;
                if (definitiveField != f) {
                    // If they refer to the _same_ instance, just keep the one we already have
                    if (alternativeField.get(optionalInstance) == f.get(optionalInstance)) skip = true;
                }
                if (skip) {
                    //nothing
                } else if (definitiveField == f) {
                    result.put(k.getName(), k);
                    configFields.put(k.getName(), f);
                } else if (definitiveField != null) {
                    if (LOG.isDebugEnabled()) LOG.debug("multiple definitions for config key {} on {}; preferring that in sub-class: {} to {}", new Object[] {
                            k.getName(), optionalInstance!=null ? optionalInstance : clazz, alternativeField, f});
                } else if (definitiveField == null) {
                    LOG.warn("multiple definitions for config key {} on {}; preferring {} to {}", new Object[] {
                            k.getName(), optionalInstance!=null ? optionalInstance : clazz, alternativeField, f});
                }
            }
            
            return result;
        } catch (IllegalAccessException e) {
            throw Throwables.propagate(e);
        }
    }
    
    /**
     * Gets the field that is in the sub-class; or null if one field does not come from a sub-class of the other field's class
     */
    // TODO Remove duplication from EntityDynamicType
    private static Field inferSubbestField(Field f1, Field f2) {
        Class<?> c1 = f1.getDeclaringClass();
        Class<?> c2 = f2.getDeclaringClass();
        boolean isSuper1 = c1.isAssignableFrom(c2);
        boolean isSuper2 = c2.isAssignableFrom(c1);
        return (isSuper1) ? (isSuper2 ? null : f2) : (isSuper2 ? f1 : null);
    }
    
}
