/FileExtraction/Java_unseen/Orange-OpenSource_fiware-cepheus/cepheus-cep/src/main/java/com/orange/cepheus/cep/EventMapper.java
package com.orange.cepheus.cep;

import com.espertech.esper.client.EventBean;
import com.fasterxml.jackson.databind.util.ISO8601DateFormat;
import com.jayway.jsonpath.*;
import com.orange.cepheus.cep.exception.ConfigurationException;
import com.orange.cepheus.cep.exception.EventProcessingException;
import com.orange.cepheus.cep.exception.TypeNotFoundException;
import com.orange.cepheus.cep.model.*;
import com.orange.cepheus.cep.model.Configuration;
import com.orange.cepheus.geo.GeoUtil;
import com.orange.ngsi.model.*;
import com.vividsolutions.jts.geom.Geometry;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Component;

import java.text.ParseException;
import java.util.*;

/**
 * Map a NGSI ContextElement to an CEP event
 */
@Component
public class EventMapper {

    private Map<String, JsonPath> jsonpaths = new HashMap<>();

    private ISO8601DateFormat iso8691DateFormat = new ISO8601DateFormat();

    /**
     * Compile JSON paths from Attributes and Metadata of the new configuration
     * @param configuration the new configuration
     */
    public void setConfiguration(Configuration configuration) throws ConfigurationException {
        Map<String, JsonPath> jsonpaths = new HashMap<>();

        for (EventTypeIn eventTypeIn : configuration.getEventTypeIns()) {
            for (Attribute attribute : eventTypeIn.getAttributes()) {
                String jsonpath = attribute.getJsonpath();
                if (jsonpath != null) {
                    // JsonPath caches paths internally, no need to reuse them from one configuration to another.
                    // Aggregate path by event type / attribute name
                    try {
                        jsonpaths.put(eventTypeIn.getType() + "/" + attribute.getName(), JsonPath.compile(jsonpath));
                    } catch (IllegalArgumentException|InvalidPathException e) {
                        throw new ConfigurationException("invalid jsonpath expression for attribute "+attribute.getName(), e);
                    }
                }

                for (Metadata metadata : attribute.getMetadata()) {
                    jsonpath = metadata.getJsonpath();
                    if (jsonpath != null) {
                        // Same as attribute but with metadata name
                        try {
                            jsonpaths.put(eventTypeIn.getType() + "/" + attribute.getName() + "/" + metadata.getName(), JsonPath.compile(jsonpath));
                        } catch (IllegalArgumentException|InvalidPathException e) {
                            throw new ConfigurationException("invalid jsonpath expression for metadata "+attribute.getName()+"/"+metadata.getName(), e);
                        }
                    }
                }
            }
        }

        this.jsonpaths = jsonpaths;
    }

    /**
     * Map an EventType go an Esper event type.
     * All properties (metadata, attributes and id) are defined at the same level.
     * Collisions might then occur, but id and then attributes will override metadata properties.
     * @param eventType the Configuration event type
     * @return a map of types
     */
    public Map<String, Object> esperTypeFromEventType(EventType eventType) {
        // Add all metadata
        Map<String, Object> properties = new HashMap<>();
        for (Attribute attribute : eventType.getAttributes()) {
            // For metadata, join with attribute name using a '_'
            for (Metadata meta : attribute.getMetadata()) {
                properties.put(attribute.getName() + "_" + meta.getName(), classForType(meta.getType()));
            }
        }
        // Override with event type properties, plus the reserved id attribute
        for (Attribute attribute : eventType.getAttributes()) {
            properties.put(attribute.getName(), classForType(attribute.getType()));
        }
        properties.put("id", String.class);
        return properties;
    }

    /**
     * Convert a NGSI Context Element to an Esper event.
     * All properties (metadata, attributes and id) are defined at the same level.
     * Collisions might then occur, but id and then attributes will override metadata properties.
     * @param contextElement the NGSI Context Element
     * @return an event to process
     * @throws EventProcessingException if the conversion fails
     */
    public Event eventFromContextElement(ContextElement contextElement) throws EventProcessingException, TypeNotFoundException {
        String eventId = contextElement.getEntityId().getId();
        String eventType = contextElement.getEntityId().getType();

        Event event = new Event(eventType);

        // Add metadata values first
        for(ContextAttribute contextAttribute : contextElement.getContextAttributeList()) {
            String attrName = contextAttribute.getName();
            for (ContextMetadata contextMetada : contextAttribute.getMetadata()) {
                String name = contextMetada.getName();
                String type = contextMetada.getType();

                Object value = contextMetada.getValue();

                // Extract value from jsonpath if any
                JsonPath jsonPath = jsonpaths.get(eventType + "/" + attrName + "/" + name);
                if (jsonPath != null) {
                    value = jsonPath.read(value);
                }

                value = valueForType(value, type, name);
                event.addValue(attrName + "_" + name, value);
            }
        }

        // Override with attributes values
        for(ContextAttribute contextAttribute : contextElement.getContextAttributeList()) {
            String name = contextAttribute.getName();
            String type = contextAttribute.getType();

            Object value = contextAttribute.getValue();

            // Extract value from jsonpath if any
            JsonPath jsonPath = jsonpaths.get(eventType + "/" + name);
            if (jsonPath != null) {
                value = jsonPath.read(value);
            }

            value = valueForType(value, type, name);
            if (value == null) {
                throw new EventProcessingException("Value cannot be null for attribute "+name);
            }

            event.addValue(name, value);
        }

        // Override with id
        event.addValue("id", eventId);

        return event;
    }

    /**
     * Convert an Esper event back to a ContextElement.
     * @param eventBean the Esper event
     * @param eventType the associated EventType
     * @return the ContextElement or null when no matching attribute in the event
     */
    public ContextElement contextElementFromEvent(EventBean eventBean, EventType eventType) {
        // When id is undefined or empty in the event, reuse the one defined in the configuration
        String id = (String)eventBean.get("id");
        if (id == null || "".equals(id)) {
            id = eventType.getId();
        }

        // Add each attribute as a tenant attribute
        List<ContextAttribute> contextAttributes = new LinkedList<>();
        for (Attribute attribute : eventType.getAttributes()) {
            String type = attribute.getType();
            String name = attribute.getName();
            Object value = eventBean.get(name);
            if (value != null) {
                value = attributeValueFromEventProperty(value, type);
                ContextAttribute contextAttribute = new ContextAttribute(name, type, value);
                // Add each metadata as a ContextMetadata of the attribute
                for (Metadata metadata : attribute.getMetadata()) {
                    String metaType = metadata.getType();
                    String metaName = metadata.getName();
                    Object metaValue = eventBean.get(name+"_"+metaName);
                    if (metaValue != null) {
                        metaValue = attributeValueFromEventProperty(metaValue, metaType);
                        contextAttribute.addMetadata(new ContextMetadata(metaName, metaType, metaValue));
                    }
                }
                contextAttributes.add(contextAttribute);
            }
        }

        // When no attributes was updated (?!), there is no point to trigger a request
        if (contextAttributes.size() == 0) {
            return null;
        }

        ContextElement contextElement = new ContextElement();
        contextElement.setEntityId(new EntityId(id, eventType.getType(), eventType.isPattern()));
        contextElement.setContextAttributeList(contextAttributes);
        return contextElement;
    }

    /**
     * Convert an EventBean property back to an ContextElement attribute.
     * Convert GeoPoint and Date to special string representation
     * @param value the value property to convert
     * @param type the type of the ContextAttribute
     * @return the ContextAttribute value
     */
    public Object attributeValueFromEventProperty(Object value, String type) {
        if ("geo:point".equals(type) && value instanceof Geometry) {
            return GeoUtil.toNGSIString((Geometry) value);
        } else if ("date".equals(type) && value instanceof Date) {
            return iso8691DateFormat.format((Date) value);
        }
        return value;
    }

    /**
     * The class corresponding to the configuration type
     * @param attributeType
     * @return
     */
    private Class classForType(String attributeType) {
        switch (attributeType) {
            case "string":
                return String.class;
            case "int":
                return int.class;
            case "long":
                return long.class;
            case "float":
                return float.class;
            case "double":
                return double.class;
            case "boolean":
                return boolean.class;
            case "date":
                return Date.class;
            case "geo:point":
                return Geometry.class;
            default:
                return Object.class;
        }
    }

    /**
     * @param value the value to convert
     * @param type NGSI type
     * @param name used for error handling
     * @return a Java Object for given value
     * @throws EventProcessingException if the conversion fails
     */
    private Object valueForType(Object value, String type, String name) throws EventProcessingException {
        // when type is not defined, handle as string
        if (type == null) {
            return value;
        }
        if (value instanceof String) {
            return valueForString((String) value, type, name);
        }
        return value;
    }

    /**
     * @param value the value to convert
     * @param type NGSI type
     * @param name used for error handling
     * @return a Java Object for given value
     * @throws EventProcessingException if the conversion fails
     */
    private Object valueForString(String value, String type, String name) throws EventProcessingException {
        // when type is not defined, handle as string
        if (type == null) {
            return value;
        }

        try {
            switch (type) {
                case "string":
                    return value;
                case "boolean":
                    return Boolean.valueOf(value);
                case "int":
                    return Integer.valueOf(value);
                case "long":
                    return Long.valueOf(value);
                case "float":
                    return Float.valueOf(value);
                case "double":
                    return Double.valueOf(value);
                case "date":
                    return iso8691DateFormat.parse(value);
                case "geo:point":
                    return GeoUtil.parseNGSIString(value);
                default:
                    return value;
            }
        } catch (IllegalArgumentException|ParseException e) {
            throw new EventProcessingException("Failed to parse value "+value+" for attribute "+name, e);
        }
    }
}
