package org.jboss.windup.graph;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;

import org.jboss.windup.graph.model.WindupVertexFrame;
import org.jboss.windup.util.Logging;
import org.jboss.windup.util.exception.WindupException;

import com.tinkerpop.blueprints.Element;
import com.tinkerpop.blueprints.Vertex;
import com.tinkerpop.frames.FramedGraph;
import com.tinkerpop.frames.modules.MethodHandler;

/**
 * Handles @MapInProperties Map<String,String>.
 *
 * @author <a href="mailto:ozizka@redhat.com">Ondrej Zizka</a>
 */
public class MapInPropertiesHandler implements MethodHandler<MapInProperties>
{
    private static final Logger log = Logging.get(MapInPropertiesHandler.class);

    @Override
    public Class<MapInProperties> getAnnotationType()
    {
        return MapInProperties.class;
    }

    @Override
    public Object processElement(Object frame, Method method, Object[] args, MapInProperties ann,
                FramedGraph<?> framedGraph, Element elm)
    {
        String methodName = method.getName();
        if (methodName.startsWith("get"))
            return handleGetter((Vertex) elm, method, args, ann, framedGraph);

        if (methodName.startsWith("set"))
            return handleSetter((Vertex) elm, method, args, ann, framedGraph);

        if (methodName.startsWith("put"))
            return handleAdder((Vertex) elm, method, args, ann, framedGraph);

        if (methodName.startsWith("putAll"))
            return handleAdder((Vertex) elm, method, args, ann, framedGraph);

        throw new WindupException("Only get*, set*, and put* method names are supported for @"
                    + MapInProperties.class.getSimpleName() + ", found at: " + method.getName());
    }

    /**
     * Getter
     */
    private Map<String, Object> handleGetter(Vertex vertex, Method method, Object[] args, MapInProperties ann, FramedGraph<?> framedGraph)
    {
        if (args != null && args.length != 0)
            throw new WindupException("Method must take zero arguments");

        Map<String, Object> map = new HashMap<>();
        String prefix = preparePrefix(ann);

        Set<String> keys = vertex.getPropertyKeys();
        for (String key : keys)
        {
            if (!key.startsWith(prefix))
                continue;
            final Object val = vertex.getProperty(key);
            if (!ann.propertyType().isAssignableFrom(val.getClass()))
            {
                log.warning("@InProperties is meant for Map<String,"+ann.propertyType().getName()+">, but the value was: " + val.getClass());
            }

            map.put(key.substring(prefix.length()), val);
        }

        return map;
    }

    /**
     * Setter
     */
    private WindupVertexFrame handleSetter(Vertex vertex, Method method, Object[] args, MapInProperties ann, FramedGraph<?> framedGraph)
    {
        // Argument.
        if (args == null || args.length != 1)
            throw new WindupException("Method must take one argument: " + method.getName());

        if (!(args[0] instanceof Map))
            throw new WindupException("Argument of " + method.getName() + " must be a Map, but is: " + args[0].getClass());

        @SuppressWarnings("unchecked")
        Map<String, Object> map = (Map<String, Object>) args[0];

        String prefix = preparePrefix(ann);

        // For all keys in the old map...
        Set<String> keys = vertex.getPropertyKeys();
        Set<String> mapKeys = map.keySet();
        for (String key : keys)
        {
            if (!key.startsWith(prefix))
                continue;
            if (WindupVertexFrame.TYPE_PROP.equals(key)) // Leave the "type" property.
                continue;
            if (key.startsWith("w:")) // Leave windup internal properties. TODO: Get the prefix from somewhere.
                continue;

            final Object val = vertex.getProperty(key);
            if (!ann.propertyType().isAssignableFrom(val.getClass()))
            {
                log.warning("@InProperties is meant for Map<String,"+ann.propertyType().getName()+">, but the value was: " + val.getClass());
            }
            String subKey = key.substring(prefix.length());
            // ...either change to new value,
            if (map.containsKey(subKey))
            {
                vertex.setProperty(key, map.get(subKey));
                mapKeys.remove(subKey);
            }
            // or remove the old.
            else
                vertex.removeProperty(key);
        }

        // Add the new entries.
        for (String key : mapKeys)
        {
            vertex.setProperty(prefix + key, map.get(key));
        }

        return null;
    }

    /**
     * Adder
     */
    private WindupVertexFrame handleAdder(Vertex vertex, Method method, Object[] args, MapInProperties ann, FramedGraph<?> framedGraph)
    {
        if (args == null || args.length != 1)
            throw new WindupException("Method must take one argument: " + method.getName());

        String prefix = preparePrefix(ann);

        // Argument.
        @SuppressWarnings("unchecked")
        Map<String, Object> map = (Map<String, Object>) args[0];

        // Store all map entries in vertex'es properties.
        for (Map.Entry<String, Object> entry : map.entrySet())
        {
            vertex.setProperty(prefix + entry.getKey(), entry.getValue());
        }

        return null;
    }

    /**
     * Returns "<ann.propertyPrefix()><SEPAR>", for example, "map:".
     */
    private String preparePrefix(MapInProperties ann)
    {
        return "".equals(ann.propertyPrefix()) ? "" : (ann.propertyPrefix() + MapInProperties.SEPAR);
    }

}
