/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF 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.apache.camel.component.spring.ws.bean;

import java.net.URI;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.camel.RuntimeCamelException;
import org.apache.camel.component.spring.ws.SpringWebserviceConsumer;
import org.apache.camel.component.spring.ws.SpringWebserviceEndpoint;
import org.apache.camel.component.spring.ws.type.EndpointMappingKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.Assert;
import org.springframework.ws.server.endpoint.MessageEndpoint;
import org.springframework.ws.soap.addressing.core.MessageAddressingProperties;
import org.springframework.ws.soap.addressing.messageid.MessageIdStrategy;
import org.springframework.ws.soap.addressing.server.AbstractAddressingEndpointMapping;
import org.springframework.ws.soap.addressing.server.AnnotationActionEndpointMapping;
import org.springframework.ws.transport.WebServiceMessageSender;

/**
 * Provides support for full WS-Addressing. Supported are faultAction and
 * response action. For more details look at @see
 * {@link AbstractAddressingEndpointMapping}. Implementation of the
 * {@link org.springframework.ws.server.EndpointMapping} consumer interface that
 * uses the camel uri to map to a WS-Addressing {@code Action} header.
 * <p/>
 */
public class WSACamelEndpointMapping extends AbstractAddressingEndpointMapping implements CamelSpringWSEndpointMapping {

    private static final Logger LOG = LoggerFactory.getLogger(WSACamelEndpointMapping.class);

    private Map<EndpointMappingKey, MessageEndpoint> endpoints = new ConcurrentHashMap<>();

    private String outputActionSuffix = AnnotationActionEndpointMapping.DEFAULT_OUTPUT_ACTION_SUFFIX;

    private String faultActionSuffix = AnnotationActionEndpointMapping.DEFAULT_OUTPUT_ACTION_SUFFIX;

    @Override
    protected Object getEndpointInternal(MessageAddressingProperties map) {
        // search the endpoint with compositeKeyFirst
        for (EndpointMappingKey key : endpoints.keySet()) {
            String compositeOrSimpleKey = null;
            switch (key.getType()) {
            case ACTION:
                compositeOrSimpleKey = getActionCompositeLookupKey(map);
                break;
            case TO:
                compositeOrSimpleKey = getToCompositeLookupKey(map);
                break;
            default:
                throw new RuntimeCamelException(
                                                "Invalid mapping type specified. Supported types are: spring-ws:action:<WS-Addressing Action>(optional:<WS-Addressing To>?<params...>\n)"
                                                    + "spring-ws:to:<WS-Addressing To>(optional:<WS-Addressing Action>?<params...>)");
            }
            // lookup for specific endpoint
            if (compositeOrSimpleKey != null && key.getLookupKey().equals(compositeOrSimpleKey)) {
                LOG.debug("Found mapping for key {}", key);
                return endpoints.get(key);
            }
        }
        
        // look up for the simple key
        for (EndpointMappingKey key : endpoints.keySet()) {
            String simpleKey = null;
            switch (key.getType()) {
            case ACTION:
                if (map.getAction() != null) {
                    simpleKey = map.getAction().toString();
                }
                break;
            case TO:
                if (map.getTo() != null) {
                    simpleKey = map.getTo().toString();
                }
                break;
            default:
                throw new RuntimeCamelException(
                                                "Invalid mapping type specified. Supported types are: spring-ws:action:<WS-Addressing Action>(optional:<WS-Addressing To>?<params...>\n)"
                                                    + "spring-ws:to:<WS-Addressing To>(optional:<WS-Addressing Action>?<params...>)");
            }
            // look up for less specific endpoint
            if (simpleKey != null && key.getLookupKey().equals(simpleKey)) {
                LOG.debug("Found mapping for key {}", key);
                return endpoints.get(key);
            }
        }
        return null;
    }

    /**
     * Generate a lookupKey for a given WS-Addressing message using action
     * property. The possible combination are:
     * <ul>
     * <li>wsaAction</li>
     * <li>wsaAction:wsaGetTo</li>
     * </ul>
     * 
     * @param map
     * @return
     */
    protected String getActionCompositeLookupKey(MessageAddressingProperties map) {
        String key = "";
        if (map.getAction() != null) {
            key = map.getAction().toString();
        }
        if (map.getTo() != null) {
            if (!key.isEmpty()) {
                key += ":";
            }
            key += map.getTo().toString();
        }
        return key;
    }

    /**
     * Generate a lookupKey for a given WS-Addressing message using getTo
     * property. The possible combination are:
     * <ul>
     * <li>wsaGetTo</li>
     * <li>wsaGetTo:wsaAction</li>
     * </ul>
     * 
     * @param map
     * @return
     */
    protected String getToCompositeLookupKey(MessageAddressingProperties map) {
        String key = "";
        if (map.getTo() != null) {
            key = map.getTo().toString();
        }
        if (map.getAction() != null) {
            if (!key.isEmpty()) {
                key += ":";
            }
            key += map.getAction().toString();
        }
        return key;
    }

    /**
     * Return output camel uri param or default action or null
     */
    @Override
    protected URI getResponseAction(Object endpoint, MessageAddressingProperties requestMap) {
        SpringWebserviceEndpoint camelEndpoint = getSpringWebserviceEndpoint(endpoint);

        URI actionUri = camelEndpoint.getConfiguration().getOutputAction();
        if (actionUri == null) {
            actionUri = getDefaultResponseAction(camelEndpoint, requestMap);
        }
        return actionUri;
    }

    /**
     * Configure message sender for wsa:replyTo from a camel route definition.
     * The route definition has priority over this endpoint.
     */
    @Override
    protected WebServiceMessageSender[] getMessageSenders(Object endpoint) {
        SpringWebserviceEndpoint camelEndpoint = getSpringWebserviceEndpoint(endpoint);

        if (camelEndpoint.getConfiguration().getMessageSender() != null) {
            return new WebServiceMessageSender[] {camelEndpoint.getConfiguration().getMessageSender()};
        }

        return super.getMessageSenders(endpoint);
    }

    /**
     * Configure message id strategy for wsa:replyTo The route definition has
     * priority over this endpoint.
     */
    @Override
    protected MessageIdStrategy getMessageIdStrategy(Object endpoint) {
        SpringWebserviceEndpoint camelEndpoint = getSpringWebserviceEndpoint(endpoint);

        if (camelEndpoint.getConfiguration().getMessageIdStrategy() != null) {
            return camelEndpoint.getConfiguration().getMessageIdStrategy();
        }

        return super.getMessageIdStrategy(endpoint);
    }

    /**
     * return fault came uri param or default fault action or null
     */
    @Override
    protected URI getFaultAction(Object endpoint, MessageAddressingProperties requestMap) {
        SpringWebserviceEndpoint camelEndpoint = getSpringWebserviceEndpoint(endpoint);

        URI actionUri = camelEndpoint.getConfiguration().getFaultAction();
        if (actionUri == null) {
            actionUri = getDefaultFaultAction(camelEndpoint, requestMap);
        }
        return actionUri;

    }

    private SpringWebserviceEndpoint getSpringWebserviceEndpoint(Object endpoint) {
        Assert.isInstanceOf(SpringWebserviceConsumer.class, endpoint, "Endpoint needs to be an instance of SpringWebserviceConsumer");

        SpringWebserviceConsumer springWebserviceConsumer = (SpringWebserviceConsumer)endpoint;
        return (SpringWebserviceEndpoint)springWebserviceConsumer.getEndpoint();
    }

    protected URI getDefaultResponseAction(Object endpoint, MessageAddressingProperties requestMap) {
        URI requestAction = requestMap.getAction();
        if (requestAction != null) {
            return URI.create(requestAction.toString() + getOutputActionSuffix());
        } else {
            return null;
        }
    }

    protected URI getDefaultFaultAction(Object endpoint, MessageAddressingProperties requestMap) {
        URI requestAction = requestMap.getAction();
        if (requestAction != null) {
            return URI.create(requestAction.toString() + getFaultActionSuffix());
        } else {
            return null;
        }
    }

    @Override
    public void addConsumer(EndpointMappingKey key, MessageEndpoint endpoint) {
        endpoints.put(key, endpoint);
    }

    @Override
    public void removeConsumer(Object key) {
        endpoints.remove(key);
    }

    /**
     * Returns the suffix to add to request <code>Action</code>s for reply
     * messages.
     */
    public String getOutputActionSuffix() {
        return outputActionSuffix;
    }

    /**
     * Sets the suffix to add to request <code>Action</code>s for reply
     * messages.
     * 
     * @see #DEFAULT_OUTPUT_ACTION_SUFFIX
     */
    public void setOutputActionSuffix(String outputActionSuffix) {
        Assert.hasText(outputActionSuffix, "'outputActionSuffix' must not be empty");
        this.outputActionSuffix = outputActionSuffix;
    }

    /**
     * Returns the suffix to add to request <code>Action</code>s for reply fault
     * messages.
     */
    public String getFaultActionSuffix() {
        return faultActionSuffix;
    }

    /**
     * Sets the suffix to add to request <code>Action</code>s for reply fault
     * messages.
     * 
     * @see #DEFAULT_FAULT_ACTION_SUFFIX
     */
    public void setFaultActionSuffix(String faultActionSuffix) {
        Assert.hasText(faultActionSuffix, "'faultActionSuffix' must not be empty");
        this.faultActionSuffix = faultActionSuffix;
    }

}
