/*
 * Copyright (c) 2015, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
 *
 * WSO2 Inc. 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.wso2.carbon.event.receiver.core.internal.type.wso2event;

import org.wso2.carbon.databridge.commons.Attribute;
import org.wso2.carbon.databridge.commons.Event;
import org.wso2.carbon.databridge.commons.StreamDefinition;
import org.wso2.carbon.event.receiver.core.InputMapper;
import org.wso2.carbon.event.receiver.core.config.EventReceiverConfiguration;
import org.wso2.carbon.event.receiver.core.config.EventReceiverConstants;
import org.wso2.carbon.event.receiver.core.config.InputMapping;
import org.wso2.carbon.event.receiver.core.config.InputMappingAttribute;
import org.wso2.carbon.event.receiver.core.config.mapping.WSO2EventInputMapping;
import org.wso2.carbon.event.receiver.core.exception.EventReceiverConfigurationException;
import org.wso2.carbon.event.receiver.core.exception.EventReceiverProcessingException;
import org.wso2.carbon.event.receiver.core.exception.EventReceiverStreamValidationException;
import org.wso2.carbon.event.receiver.core.internal.ds.EventReceiverServiceValueHolder;
import org.wso2.carbon.event.receiver.core.internal.util.EventReceiverUtil;
import org.wso2.carbon.event.receiver.core.internal.util.helper.EventReceiverConfigurationHelper;
import org.wso2.carbon.event.stream.core.EventStreamService;
import org.wso2.carbon.event.stream.core.exception.EventStreamConfigurationException;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

public class WSO2EventInputMapper implements InputMapper {
    private EventReceiverConfiguration eventReceiverConfiguration = null;
    private StreamDefinition exportedStreamDefinition = null;
    private StreamDefinition importedStreamDefinition = null;
    private Map<InputDataType, int[]> inputDataTypeSpecificPositionMap = null;
    private boolean arbitraryMapsEnabled = false;
    private int inputStreamSize = 0;
    private int outMetaSize = 0, outCorrelationSize = 0, outPayloadSize = 0;

    public WSO2EventInputMapper(EventReceiverConfiguration eventReceiverConfiguration,
                                StreamDefinition exportedStreamDefinition)
            throws EventReceiverConfigurationException {
        //TODO It is better if logic to check from stream definition store can be moved outside of input mapper.
        EventStreamService eventStreamService = EventReceiverServiceValueHolder.getEventStreamService();

        this.eventReceiverConfiguration = eventReceiverConfiguration;
        WSO2EventInputMapping wso2EventInputMapping = (WSO2EventInputMapping) this.eventReceiverConfiguration.getInputMapping();

        String fromStreamName = wso2EventInputMapping.getFromEventName();
        String fromStreamVersion = wso2EventInputMapping.getFromEventVersion();
        this.arbitraryMapsEnabled = wso2EventInputMapping.isArbitraryMapsEnabled();

        if (fromStreamName == null || fromStreamVersion == null || (fromStreamName.isEmpty()) || (fromStreamVersion.isEmpty())) {
            importedStreamDefinition = exportedStreamDefinition;
        } else {
            try {
                this.importedStreamDefinition = eventStreamService.getStreamDefinition(fromStreamName, fromStreamVersion);
            } catch (EventStreamConfigurationException e) {
                throw new EventReceiverStreamValidationException("Error while retrieving stream definition : " + e.getMessage(), fromStreamName + ":" + fromStreamVersion);
            }
        }
        this.exportedStreamDefinition = exportedStreamDefinition;
        if (importedStreamDefinition == null) {
            // Import stream for custom mapping is not available
            String fromStream = fromStreamName + EventReceiverConstants.STREAM_NAME_VER_DELIMITER + fromStreamVersion;
            throw new EventReceiverStreamValidationException("Imported stream " + fromStream + " is not available", fromStream);
        }
        boolean candidateForArbitraryMaps = isCandidateForArbitraryAttributes(eventReceiverConfiguration.getInputMapping(), importedStreamDefinition);
        if (!candidateForArbitraryMaps) {
            validateInputStreamAttributes();
        }

        if (importedStreamDefinition != null && eventReceiverConfiguration.getInputMapping().isCustomMappingEnabled() && !candidateForArbitraryMaps) {
            this.inputDataTypeSpecificPositionMap = new HashMap<InputDataType, int[]>();
            Map<Integer, Integer> payloadDataMap = new TreeMap<Integer, Integer>();
            Map<Integer, Integer> metaDataMap = new TreeMap<Integer, Integer>();
            Map<Integer, Integer> correlationDataMap = new TreeMap<Integer, Integer>();

            List<Attribute> allAttributes = new ArrayList<Attribute>();
            if (importedStreamDefinition.getMetaData() != null && !importedStreamDefinition.getMetaData().isEmpty()) {
                allAttributes.addAll(importedStreamDefinition.getMetaData());
            }
            if (importedStreamDefinition.getCorrelationData() != null && !importedStreamDefinition.getCorrelationData().isEmpty()) {
                allAttributes.addAll(importedStreamDefinition.getCorrelationData());
            }
            if (importedStreamDefinition.getPayloadData() != null && !importedStreamDefinition.getPayloadData().isEmpty()) {
                allAttributes.addAll(importedStreamDefinition.getPayloadData());
            }
            int metaCount = 0, correlationCount = 0, payloadCount = 0;
            for (InputMappingAttribute inputMappingAttribute : wso2EventInputMapping.getInputMappingAttributes()) {
                if (inputMappingAttribute.getToElementKey().startsWith(EventReceiverConstants.META_DATA_PREFIX)) {
                    for (int i = 0; i < allAttributes.size(); i++) {
                        if (allAttributes.get(i).getName().equals(inputMappingAttribute.getFromElementKey())) {
                            metaDataMap.put(metaCount, i);
                            break;
                        }
                    }
                    if (metaDataMap.get(metaCount++) == null) {
                        this.inputDataTypeSpecificPositionMap = null;
                        throw new EventReceiverStreamValidationException("Cannot find a corresponding meta data input attribute '"
                                + inputMappingAttribute.getFromElementKey() + "' in stream with id " + importedStreamDefinition.getStreamId(), importedStreamDefinition.getStreamId());
                    }
                } else if (inputMappingAttribute.getToElementKey().startsWith(EventReceiverConstants.CORRELATION_DATA_PREFIX)) {
                    for (int i = 0; i < allAttributes.size(); i++) {
                        if (allAttributes.get(i).getName().equals(inputMappingAttribute.getFromElementKey())) {
                            correlationDataMap.put(correlationCount, i);
                            break;
                        }
                    }
                    if (correlationDataMap.get(correlationCount++) == null) {
                        this.inputDataTypeSpecificPositionMap = null;
                        throw new EventReceiverStreamValidationException("Cannot find a corresponding correlation data input attribute '"
                                + inputMappingAttribute.getFromElementKey() + "' in stream with id " + importedStreamDefinition.getStreamId(), importedStreamDefinition.getStreamId());
                    }
                } else {
                    for (int i = 0; i < allAttributes.size(); i++) {
                        if (allAttributes.get(i).getName().equals(inputMappingAttribute.getFromElementKey())) {
                            payloadDataMap.put(payloadCount, i);
                            break;
                        }
                    }
                    if (payloadDataMap.get(payloadCount++) == null) {
                        this.inputDataTypeSpecificPositionMap = null;
                        throw new EventReceiverStreamValidationException("Cannot find a corresponding payload data input attribute '"
                                + inputMappingAttribute.getFromElementKey() + "' in stream with id : " + importedStreamDefinition.getStreamId(), importedStreamDefinition.getStreamId());
                    }
                }
            }
            outMetaSize = metaDataMap.size();
            outCorrelationSize = correlationDataMap.size();
            outPayloadSize = payloadDataMap.size();
            int[] metaPositions = new int[outMetaSize];
            for (int i = 0; i < metaPositions.length; i++) {
                metaPositions[i] = metaDataMap.get(i);
            }
            inputDataTypeSpecificPositionMap.put(InputDataType.META_DATA, metaPositions);
            int[] correlationPositions = new int[outCorrelationSize];
            for (int i = 0; i < correlationPositions.length; i++) {
                correlationPositions[i] = correlationDataMap.get(i);
            }
            inputDataTypeSpecificPositionMap.put(InputDataType.CORRELATION_DATA, correlationPositions);
            int[] payloadPositions = new int[outPayloadSize];
            for (int i = 0; i < payloadPositions.length; i++) {
                payloadPositions[i] = payloadDataMap.get(i);
            }
            inputDataTypeSpecificPositionMap.put(InputDataType.PAYLOAD_DATA, payloadPositions);
            inputStreamSize = allAttributes.size();
        } else if (importedStreamDefinition != null && (!eventReceiverConfiguration.getInputMapping().isCustomMappingEnabled())) {
            if (importedStreamDefinition.getCorrelationData() != null ? !importedStreamDefinition.getCorrelationData().equals(exportedStreamDefinition.getCorrelationData()) : exportedStreamDefinition.getCorrelationData() != null) {
                throw new EventReceiverStreamValidationException("Input stream definition : " + importedStreamDefinition + " not matching with output stream definition : " + exportedStreamDefinition + " to create pass-through link ", importedStreamDefinition.getStreamId());
            }
            if (importedStreamDefinition.getMetaData() != null ? !importedStreamDefinition.getMetaData().equals(exportedStreamDefinition.getMetaData()) : exportedStreamDefinition.getMetaData() != null) {
                throw new EventReceiverStreamValidationException("Input stream definition : " + importedStreamDefinition + " not matching with output stream definition : " + exportedStreamDefinition + " to create pass-through link ", importedStreamDefinition.getStreamId());
            }
            if (importedStreamDefinition.getPayloadData() != null ? !importedStreamDefinition.getPayloadData().equals(exportedStreamDefinition.getPayloadData()) : exportedStreamDefinition.getPayloadData() != null) {
                throw new EventReceiverStreamValidationException("Input stream definition : " + importedStreamDefinition + " not matching with output stream definition : " + exportedStreamDefinition + " to create pass-through link ", importedStreamDefinition.getStreamId());
            }
        } else if (!candidateForArbitraryMaps) {
            throw new EventReceiverStreamValidationException("Error while retrieving stream definition : " + fromStreamName + ":" + fromStreamVersion);
        }
    }

    private boolean isCandidateForArbitraryAttributes(InputMapping inputMapping, StreamDefinition importedStreamDefinition) {
        for (InputMappingAttribute inputMappingAttribute : inputMapping.getInputMappingAttributes()) {
            if (EventReceiverConstants.META_DATA_VAL.equals(inputMappingAttribute.getFromElementType()) &&
                    !importedStreamDefinition.getMetaData().contains(new Attribute(inputMappingAttribute.getFromElementKey(),
                            inputMappingAttribute.getToElementType()))) {
                return true;
            }
            if (EventReceiverConstants.CORRELATION_DATA_VAL.equals(inputMappingAttribute.getFromElementType()) &&
                    !importedStreamDefinition.getCorrelationData().contains(new Attribute(inputMappingAttribute.getFromElementKey(),
                            inputMappingAttribute.getToElementType()))) {
                return true;
            }
            if (EventReceiverConstants.PAYLOAD_DATA_VAL.equals(inputMappingAttribute.getFromElementType()) &&
                    !importedStreamDefinition.getPayloadData().contains(new Attribute(inputMappingAttribute.getFromElementKey(),
                            inputMappingAttribute.getToElementType()))) {
                return true;
            }
        }
        return false;
    }

    private void validateInputStreamAttributes()
            throws EventReceiverConfigurationException {
        if (this.importedStreamDefinition != null) {
            WSO2EventInputMapping wso2EventInputMapping = (WSO2EventInputMapping) eventReceiverConfiguration.getInputMapping();
            List<InputMappingAttribute> inputMappingAttributes = wso2EventInputMapping.getInputMappingAttributes();
            List<String> inputStreamMetaAttributeNames = getAttributeNamesList(importedStreamDefinition.getMetaData());
            List<String> inputStreamCorrelationAttributeNames = getAttributeNamesList(importedStreamDefinition.getCorrelationData());
            List<String> inputStreamPayloadAttributeNames = getAttributeNamesList(importedStreamDefinition.getPayloadData());

            for (InputMappingAttribute inputMappingAttribute : inputMappingAttributes) {
                if (inputMappingAttribute.getToElementKey().startsWith(EventReceiverConstants.META_DATA_PREFIX)
                        && !inputStreamMetaAttributeNames.contains(inputMappingAttribute.getFromElementKey())) {
                    throw new EventReceiverStreamValidationException("Property " + inputMappingAttribute.getFromElementKey()
                            + " is not in the input stream definition. ", importedStreamDefinition.getStreamId());
                } else if (inputMappingAttribute.getToElementKey().startsWith(EventReceiverConstants.CORRELATION_DATA_PREFIX)
                        && !inputStreamCorrelationAttributeNames.contains(inputMappingAttribute.getFromElementKey())) {
                    throw new EventReceiverStreamValidationException("Property " + inputMappingAttribute.getFromElementKey()
                            + " is not in the input stream definition. ", importedStreamDefinition.getStreamId());
                } else if (!inputMappingAttribute.getToElementKey().startsWith(EventReceiverConstants.META_DATA_PREFIX)
                        && !inputMappingAttribute.getToElementKey().startsWith(EventReceiverConstants.CORRELATION_DATA_PREFIX)
                        && !inputStreamPayloadAttributeNames.contains(inputMappingAttribute.getFromElementKey())) {
                    throw new EventReceiverStreamValidationException("Property " + inputMappingAttribute.getFromElementKey()
                            + " is not in the input stream definition. ", importedStreamDefinition.getStreamId());
                }
            }
        }
    }

    private List<String> getAttributeNamesList(List<Attribute> attributeList) {
        List<String> attributeNamesList = new ArrayList<String>();
        if (attributeList != null) {
            for (Attribute attribute : attributeList) {
                attributeNamesList.add(attribute.getName());
            }
        }

        return attributeNamesList;
    }

    @Override
    public Object convertToMappedInputEvent(Object obj) throws EventReceiverProcessingException {
        if (obj instanceof Event) {
            Event event = (Event) obj;
            Map<String, String> arbitraryMap = event.getArbitraryDataMap();
            if (arbitraryMap != null && !arbitraryMap.isEmpty()) {
                return processArbitraryMap(event);
            } else if (inputDataTypeSpecificPositionMap != null) {
                Object[] outMetaAttrArray = new Object[outMetaSize];
                Object[] outCorrelationAttrArray = new Object[outCorrelationSize];
                Object[] outPayloadAttrArray = new Object[outPayloadSize];
                // Construct input event as an array of attributes
                Object[] inEventArray = new Object[inputStreamSize];
                int inEventArrayCount = 0;
                if (event.getMetaData() != null) {
                    for (Object attribute : event.getMetaData()) {
                        inEventArray[inEventArrayCount++] = attribute;
                    }
                }
                if (event.getCorrelationData() != null) {
                    for (Object attribute : event.getCorrelationData()) {
                        inEventArray[inEventArrayCount++] = attribute;
                    }
                }
                if (event.getPayloadData() != null) {
                    for (Object attribute : event.getPayloadData()) {
                        inEventArray[inEventArrayCount++] = attribute;
                    }
                }
                // Finished construction of input event array

                int[] metaPositions = inputDataTypeSpecificPositionMap.get(InputDataType.META_DATA);
                for (int i = 0; i < metaPositions.length; i++) {
                    outMetaAttrArray[i] = inEventArray[metaPositions[i]];
                }
                int[] correlationPositions = inputDataTypeSpecificPositionMap.get(InputDataType.CORRELATION_DATA);
                for (int i = 0; i < correlationPositions.length; i++) {
                    outCorrelationAttrArray[i] = inEventArray[correlationPositions[i]];
                }
                int[] payloadPositions = inputDataTypeSpecificPositionMap.get(InputDataType.PAYLOAD_DATA);
                for (int i = 0; i < payloadPositions.length; i++) {
                    outPayloadAttrArray[i] = inEventArray[payloadPositions[i]];
                }
                return new Event(exportedStreamDefinition.getStreamId(), event.getTimeStamp(), outMetaAttrArray,
                        outCorrelationAttrArray, outPayloadAttrArray);
            } else {
                return null;
            }
        }
        return null;
    }

    @Override
    public Object convertToTypedInputEvent(Object obj) throws EventReceiverProcessingException {
        if (obj instanceof Event) {
            return obj;
        } else {
            return null;
        }
    }

    @Override
    public Attribute[] getOutputAttributes() {
        WSO2EventInputMapping wso2EventInputMapping = (WSO2EventInputMapping) eventReceiverConfiguration.getInputMapping();
        List<InputMappingAttribute> inputMappingAttributes = wso2EventInputMapping.getInputMappingAttributes();
        return EventReceiverConfigurationHelper.getAttributes(inputMappingAttributes);
    }

    //TODO This method needs to be optimized for performance
    private Event processArbitraryMap(Event inEvent) {
        Map<String, String> arbitraryMap = inEvent.getArbitraryDataMap();
        Object[] metaData, correlationData, payloadData;
        if (exportedStreamDefinition.getMetaData() != null) {
            metaData = new Object[exportedStreamDefinition.getMetaData().size()];
        } else {
            metaData = new Object[0];
        }
        if (exportedStreamDefinition.getCorrelationData() != null) {
            correlationData = new Object[exportedStreamDefinition.getCorrelationData().size()];
        } else {
            correlationData = new Object[0];
        }
        if (exportedStreamDefinition.getPayloadData() != null) {
            payloadData = new Object[exportedStreamDefinition.getPayloadData().size()];
        } else {
            payloadData = new Object[0];
        }
        for (int i = 0; i < metaData.length; i++) {
            Attribute metaAttribute = exportedStreamDefinition.getMetaData().get(i);
            String value = arbitraryMap.get(EventReceiverUtil.getMappedInputStreamAttributeName(
                    metaAttribute.getName(), eventReceiverConfiguration.getInputMapping()));
            if (value != null) {
                Object attributeObj = EventReceiverUtil.getConvertedAttributeObject(value, metaAttribute.getType());
                metaData[i] = attributeObj;
            } else {
                metaData[i] = inEvent.getMetaData()[i];
            }
        }
        for (int i = 0; i < correlationData.length; i++) {
            Attribute correlationAttribute = exportedStreamDefinition.getCorrelationData().get(i);
            String value = arbitraryMap.get(EventReceiverUtil.getMappedInputStreamAttributeName(
                    correlationAttribute.getName(), eventReceiverConfiguration.getInputMapping()));
            if (value != null) {
                Object attributeObj = EventReceiverUtil.getConvertedAttributeObject(value, correlationAttribute.getType());
                correlationData[i] = attributeObj;
            } else {
                correlationData[i] = inEvent.getCorrelationData()[i];
            }
        }
        for (int i = 0; i < payloadData.length; i++) {
            Attribute payloadAttribute = exportedStreamDefinition.getPayloadData().get(i);
            String value = arbitraryMap.get(EventReceiverUtil.getMappedInputStreamAttributeName(
                    payloadAttribute.getName(), eventReceiverConfiguration.getInputMapping()));
            if (value != null) {
                Object attributeObj = EventReceiverUtil.getConvertedAttributeObject(value, payloadAttribute.getType());
                payloadData[i] = attributeObj;
            } else {
                payloadData[i] = inEvent.getPayloadData()[i];
            }
        }
        return new Event(inEvent.getStreamId(), inEvent.getTimeStamp(), metaData, correlationData, payloadData, arbitraryMap);
    }

    private enum InputDataType {
        META_DATA, CORRELATION_DATA, PAYLOAD_DATA
    }

}
