// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.processing.request.properties;

import com.yahoo.component.provider.FreezableClass;
import com.yahoo.processing.request.CompoundName;
import com.yahoo.processing.request.Properties;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.logging.Logger;

/**
 * A HashMap backing of Properties.
 * <p>
 * When this is cloned it will deep copy not only the model object map, but also each
 * clonable member inside the map.
 * <p>
 * Subclassing is supported, a hook can be implemented to provide conditional inclusion in the map.
 * By default - all properties are accepted, so set is never propagated.
 * <p>
 * This class is not multithread safe.
 *
 * @author bratseth
 */
public class PropertyMap extends Properties {

    private static Logger log = Logger.getLogger(PropertyMap.class.getName());

    /**
     * The properties of this
     */
    private Map<CompoundName, Object> properties = new HashMap<>();

    public void set(CompoundName name, Object value, Map<String, String> context) {
        if (shouldSet(name, value))
            properties.put(name, value);
        else
            super.set(name, value, context);
    }

    /**
     * Return true if this value should be set in this map, false if the set should be propagated instead
     * This default implementation always returns true.
     */
    protected boolean shouldSet(CompoundName name, Object value) {
        return true;
    }

    public
    @Override
    Object get(CompoundName name, Map<String, String> context,
               com.yahoo.processing.request.Properties substitution) {
        if (!properties.containsKey(name)) return super.get(name, context, substitution);
        return properties.get(name);
    }

    public
    @Override
    PropertyMap clone() {
        PropertyMap clone = (PropertyMap) super.clone();
        clone.properties = cloneMap(this.properties);
        return clone;
    }

    /**
     * Clones a map by deep cloning each value which is cloneable and shallow copying all other values.
     */
    public static Map<CompoundName, Object> cloneMap(Map<CompoundName, Object> map) {
        Map<CompoundName, Object> cloneMap = new HashMap<>();
        for (Map.Entry<CompoundName, Object> entry : map.entrySet()) {
            Object cloneValue = clone(entry.getValue());
            if (cloneValue == null)
                cloneValue = entry.getValue(); // Shallow copy objects which does not support cloning
            cloneMap.put(entry.getKey(), cloneValue);
        }
        return cloneMap;
    }

    /**
     * Clones this object if it is clonable, and the clone is public. Returns null if not
     */
    public static Object clone(Object object) {
        if (object == null) return null;
        if (!(object instanceof Cloneable)) return null;
        if (object instanceof Object[])
            return arrayClone((Object[]) object);
        else
            return objectClone(object);
    }

    private static Object arrayClone(Object[] object) {
        Object[] arrayClone = Arrays.copyOf(object, object.length);
        // deep clone
        for (int i = 0; i < arrayClone.length; i++) {
            Object elementClone = clone(arrayClone[i]);
            if (elementClone != null)
                arrayClone[i] = elementClone;
        }
        return arrayClone;
    }

    private static Object objectClone(Object object) {
        // Fastpath for our own commonly used classes
        if (object instanceof FreezableClass) {
            // List common superclass of 'com.yahoo.search.result.Hit'
            return ((FreezableClass) object).clone();
        }
        else if (object instanceof PublicCloneable) {
            return ((PublicCloneable)object).clone();
        }
        else if (object instanceof LinkedList) { // TODO: Why? Somebody's infatuation with LinkedList knows no limits
            return ((LinkedList) object).clone();
        }

        try {
            Method cloneMethod = object.getClass().getMethod("clone");
            return cloneMethod.invoke(object);
        } catch (NoSuchMethodException e) {
            log.warning("'" + object + "' is Cloneable, but has no clone method - will use the same instance in all requests");
            return null;
        } catch (IllegalAccessException e) {
            log.warning("'" + object + "' is Cloneable, but clone method cannot be accessed - will use the same instance in all requests");
            return null;
        } catch (InvocationTargetException e) {
            throw new RuntimeException("Exception cloning '" + object + "'", e);
        }
    }

    @Override
    public Map<String, Object> listProperties(CompoundName path, Map<String, String> context, Properties substitution) {
        Map<String, Object> map = super.listProperties(path, context, substitution);

        for (Map.Entry<CompoundName, Object> entry : properties.entrySet()) {
            if ( ! entry.getKey().hasPrefix(path)) continue;
            CompoundName propertyName = entry.getKey().rest(path.size());
            if (propertyName.isEmpty()) continue;
            map.put(propertyName.toString(), entry.getValue());
        }
        return map;
    }

}

@@ -153,24 +153,66 @@
 roperties;%0A%0A
+import com.yahoo.collections.MethodCache;%0A
 import com.y
@@ -456,91 +456,9 @@
 til.
-Arrays;%0Aimport java.util.HashMap;%0Aimport java.util.LinkedList;%0Aimport java.util.Map
+*
 ;%0Aim
@@ -1035,16 +1035,98 @@
 Name());
+%0A    private static final MethodCache cloneMethodCache = new MethodCache(%22clone%22);
 %0A%0A    /*
@@ -4225,22 +4225,78 @@
   %7D%0A
-%0A
+ 
        
- try %7B
+else if (object instanceof ArrayList) %7B // TODO: Why? Likewise
 %0A   
@@ -4308,64 +4308,42 @@
     
-Method cloneMethod = object.getClass().getMethod(%22
+return ((ArrayList) object).
 clone
-%22
+(
 );%0A 
@@ -4353,87 +4353,123 @@
     
+%7D%0A%0A
     
-return cloneMethod.invoke(object);%0A        %7D catch (NoSuchMethodException e
+    try %7B%0A            Method cloneMethod = cloneMethodCache.get(object);%0A            if (cloneMethod == null
 ) %7B%0A
@@ -4472,32 +4472,36 @@
 ) %7B%0A            
+    
 log.warning(%22'%22 
@@ -4604,32 +4604,36 @@
 %22);%0A            
+    
 return null;%0A   
@@ -4621,32 +4621,93 @@
    return null;%0A
+            %7D%0A            return cloneMethod.invoke(object);%0A
         %7D catch 

Optimize objectclone with a general method cache and special handling of ArrayList