/*
 * Copyright 2013 Red Hat Inc. and/or its affiliates and other contributors.
 *
 * Licensed 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.switchyard.component.camel.switchyard;

import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.activation.DataHandler;

import org.apache.camel.impl.DefaultMessage;
import org.switchyard.ExchangePhase;
import org.switchyard.Property;
import org.switchyard.Scope;
import org.switchyard.common.camel.ContextPropertyUtil;
import org.switchyard.common.camel.HandlerDataSource;
import org.switchyard.common.camel.SwitchYardMessage;
import org.switchyard.label.BehaviorLabel;

/**
 * ExchangeMapper handles the mapping between SwitchYard and Camel exchanges when used from a Camel
 * implementation.  This mapping is distinct from what message composers do with gateways for two
 * reasons: 
 * 1) Gateway composers are configurable by users. Internal mapping between exchanges
 * within a camel implementation should not be configurable.
 * 2) The mapping rules for gateways and implementations can differ in subtle ways.  For example,
 * transient context properties propagate from gateway routes with SwitchYardProducer, but this should
 * not happen in implementation routes as it allows exchange properties to leak between multiple 
 * switchyard:// endpoint invocations.
 */
public final class ExchangeMapper {
    
    /**
     * Name of List<String> exchange property with names of properties to include in mapping.  By
     * default, all Camel* exchange properties are excluded.
     */
    public static final String CAMEL_PROPERTY_INCLUDES = "org.switchyard.includedCamelProperties";
    private static final String CAMEL_PROPERTY_PREFIX = "Camel";
    
    private ExchangeMapper() {
        
    }

    /**
     * Map from a camel exchange to a SwitchYard exchange.
     * @param camelExchange the camel exchange
     * @param syExchange the switchyard exchange
     * @param phase ExchangePhase.IN to target camelExchange.getIn(), ExchangePhase.OUT to 
     * target camelExchange.getIn().
     * @return the 
     */
    public static org.switchyard.Message mapCamelToSwitchYard(
            org.apache.camel.Exchange camelExchange,
            org.switchyard.Exchange syExchange,
            ExchangePhase phase) {
        
        // Associate the correct Camel message with the SY exchange
        org.switchyard.Message message = syExchange.createMessage();
        org.apache.camel.Message camelMessage =
                mapCamelPropertiesToSwitchYard(camelExchange, message.getContext(), phase);
        message.setContent(camelMessage.getBody());
        
        for (String attachmentName : camelMessage.getAttachmentNames()) {
            message.addAttachment(attachmentName, new HandlerDataSource(camelMessage.getAttachment(attachmentName)));
        }
        
        return message;
    }
    
    /**
     * Get a camel message from given exchange, IN message for ExchangePhase.IN, OUT message for ExchangePhase.OUT.
     * @param camelExchange the camel exchange
     * @param phase ExchangePhase.IN to target camelExchange.getIn(), ExchangePhase.OUT to 
     * target camelExchange.getIn().
     * @return camel message
     */
    public static org.apache.camel.Message getCamelMessage(
            org.apache.camel.Exchange camelExchange,
            ExchangePhase phase) {
        
        if (phase.equals(ExchangePhase.OUT) && camelExchange.hasOut()) {
            return camelExchange.getOut();
        } else {
            return camelExchange.getIn();
        }
    }
    
    /**
     * Map from camel exchange properties and message headers to SwitchYard context property.
     * @param camelExchange the camel exchange
     * @param syContext switchyard context
     * @param phase ExchangePhase.IN to target camelExchange.getIn(), ExchangePhase.OUT to 
     * target camelExchange.getIn().
     * @return A camel message targeted to retrieve message headers
     */
    public static org.apache.camel.Message mapCamelPropertiesToSwitchYard(
            org.apache.camel.Exchange camelExchange,
            org.switchyard.Context syContext,
            ExchangePhase phase) {
        
        for (String property : camelExchange.getProperties().keySet()) {
            if (ContextPropertyUtil.isReservedProperty(property, Scope.EXCHANGE)) {
                continue;
            }
            // skip the implementation route property as we don't want that propagated
            if (SwitchYardConsumer.IMPLEMENTATION_ROUTE.equals(property)) {
                continue;
            }
            // skip Camel* properties unless they have been explicitly included
            List<?> propertyIncludes = camelExchange.getProperty(CAMEL_PROPERTY_INCLUDES, List.class);
            if (property.startsWith(CAMEL_PROPERTY_PREFIX) 
                    && (propertyIncludes == null || !propertyIncludes.contains(property))) {
                continue;
            }
            
            syContext.setProperty(property, camelExchange.getProperty(property), Scope.EXCHANGE);
        }
        
        org.apache.camel.Message camelMessage = getCamelMessage(camelExchange, phase);
        Set<Map.Entry<String, Object>> entrySet = camelMessage.getHeaders().entrySet();  
        Iterator<Map.Entry<String, Object>> iter = entrySet.iterator();  
        while (iter.hasNext()) {  
            String header = iter.next().getKey();
            if (ContextPropertyUtil.isReservedProperty(header, Scope.MESSAGE)) {
                continue;
            }
            syContext.setProperty(header, camelMessage.getHeader(header), Scope.MESSAGE);
        }
        
        return camelMessage;
    }
    
    /**
     * Maps a SwitchYard exchange to a Camel exchange.  Keep in mind that the camel message created
     * during mapping is *not* associate with the exchange.  You need to call setIn() or setOut() 
     * with the returned reference depending on your use case.
     * @param syExchange switchyard exchange
     * @param camelExchange camel exchange
     * @return mapped camel message
     */
    public static DefaultMessage mapSwitchYardToCamel(
            org.switchyard.Exchange syExchange,
            org.apache.camel.Exchange camelExchange) {
        
        DefaultMessage camelMessage = new SwitchYardMessage();
        camelMessage.setBody(syExchange.getMessage().getContent());
        
        mapSwitchYardPropertiesToCamel(syExchange.getContext(), camelExchange, camelMessage);
        
        for (String attachmentName : syExchange.getMessage().getAttachmentMap().keySet()) {
            camelMessage.addAttachment(attachmentName, 
                    new DataHandler(syExchange.getMessage().getAttachment(attachmentName)));
        }
        
        return camelMessage;
    }
    
    /**
     * Map from SwitchYard context property to camel exchange property or camel message header.
     * @param syContext switchyard context
     * @param camelExchange camel exchange
     * @param camelMessage camel message
     */
    public static void mapSwitchYardPropertiesToCamel(
            org.switchyard.Context syContext,
            org.apache.camel.Exchange camelExchange,
            org.apache.camel.Message camelMessage) {
        mapSwitchYardPropertiesToCamel(syContext, camelExchange, camelMessage, false);
    }
    
    /**
     * Map from SwitchYard context property to camel exchange property or camel message header.
     * @param syContext switchyard context
     * @param camelExchange camel exchange
     * @param camelMessage camel message
     * @param skipReserved true if skip internal properties
     */
    public static void mapSwitchYardPropertiesToCamel(
            org.switchyard.Context syContext,
            org.apache.camel.Exchange camelExchange,
            org.apache.camel.Message camelMessage,
            boolean skipReserved) {
        
        for (Property property : syContext.getProperties()) {
            if (skipReserved) {
                if (ContextPropertyUtil.isReservedProperty(property.getName(), Scope.EXCHANGE)) {
                    continue;
                }
                // skip the implementation route property as we don't want that propagated
                if (SwitchYardConsumer.IMPLEMENTATION_ROUTE.equals(property.getName())) {
                    continue;
                }
                // skip Camel* properties unless they have been explicitly included
                List<?> propertyIncludes = camelExchange.getProperty(CAMEL_PROPERTY_INCLUDES, List.class);
                if (property.getName().startsWith(CAMEL_PROPERTY_PREFIX) 
                        && (propertyIncludes == null || !propertyIncludes.contains(property.getName()))) {
                    continue;
                }
            }
            if (property.hasLabel(BehaviorLabel.TRANSIENT.label()) 
                    || ContextPropertyUtil.isReservedProperty(property.getName(), property.getScope())) {
                continue;
            }
            
            if (Scope.EXCHANGE.equals(property.getScope())) {
                camelExchange.setProperty(property.getName(), property.getValue());
            } else if (camelMessage != null) {
                camelMessage.setHeader(property.getName(), property.getValue());
            }
        }
    }
}
