/*
 * 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.ode.jbi;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.ode.bpel.iapi.Endpoint;
import org.apache.ode.bpel.iapi.Message;
import org.apache.ode.bpel.iapi.MessageExchange.Status;
import org.apache.ode.bpel.iapi.MyRoleMessageExchange;
import org.apache.ode.jbi.msgmap.Mapper;
import org.apache.ode.jbi.msgmap.MessageTranslationException;
import org.w3c.dom.Element;

import javax.jbi.JBIException;
import javax.jbi.messaging.ExchangeStatus;
import javax.jbi.messaging.Fault;
import javax.jbi.messaging.InOnly;
import javax.jbi.messaging.InOut;
import javax.jbi.messaging.MessagingException;
import javax.jbi.messaging.NormalizedMessage;
import javax.jbi.servicedesc.ServiceEndpoint;
import javax.xml.namespace.QName;
import java.util.HashMap;
import java.util.Map;

/**
 * Bridge JBI (consumer) to ODE (provider).
 */
public class OdeService extends ServiceBridge implements JbiMessageExchangeProcessor {

    private static final Log __log = LogFactory.getLog(OdeService.class);

    /** utility for tracking outstanding JBI message exchanges. */
    private final JbiMexTracker _jbiMexTracker = new JbiMexTracker();

    /** JBI-Generated Endpoint */
    private ServiceEndpoint _internal;

    /** External endpoint. */
    private ServiceEndpoint _external;

    private OdeContext _ode;

    private Element _serviceref;

    private Endpoint _endpoint;

    public OdeService(OdeContext odeContext, Endpoint endpoint) throws Exception {
        _ode = odeContext;
        _endpoint = endpoint;
    }

    /**
     * Do the JBI endpoint activation.
     * 
     * @throws JBIException
     */
    public void activate() throws JBIException {
        if (_serviceref == null) {
            ServiceEndpoint[] candidates = _ode.getContext().getExternalEndpointsForService(_endpoint.serviceName);
            if (candidates.length != 0) {
                _external = candidates[0];
            }
        }
        _internal = _ode.getContext().activateEndpoint(_endpoint.serviceName, _endpoint.portName);
        if (__log.isDebugEnabled()) {
            __log.debug("Activated endpoint " + _endpoint);
        }
        // TODO: Is there a race situation here?
    }

    /**
     * Deactivate endpoints in JBI.
     */
    public void deactivate() throws JBIException {
        _ode.getContext().deactivateEndpoint(_internal);
        __log.debug("Dectivated endpoint " + _endpoint);
    }

    public ServiceEndpoint getInternalServiceEndpoint() {
        return _internal;
    }

    public ServiceEndpoint getExternalServiceEndpoint() {
        return _external;
    }

    public void onJbiMessageExchange(javax.jbi.messaging.MessageExchange jbiMex) throws MessagingException {
        if (jbiMex.getRole() != javax.jbi.messaging.MessageExchange.Role.PROVIDER) {
            String errmsg = "Message exchange is not in PROVIDER role as expected: " + jbiMex.getExchangeId();
            __log.fatal(errmsg);
            throw new IllegalArgumentException(errmsg);
        }

        if (jbiMex.getStatus() != ExchangeStatus.ACTIVE) {
            // We can forget about the exchange.
            __log.debug("Consuming MEX tracker " + jbiMex.getExchangeId());
            _jbiMexTracker.consume(jbiMex.getExchangeId());
            return;
        }

        if (jbiMex.getOperation() == null) {
            throw new IllegalArgumentException("Null operation in JBI message exchange id=" + jbiMex.getExchangeId()
                                                + " endpoint=" + _endpoint);
        }

        if (jbiMex.getPattern().equals(org.apache.ode.jbi.MessageExchangePattern.IN_ONLY)) {
            boolean success = false;
            Exception err = null;
            try {
                invokeOde(jbiMex, ((InOnly) jbiMex).getInMessage());
                success = true;
            } catch (Exception ex) {
                __log.error("Error invoking ODE.", ex);
                err = ex;
            } finally {
                if (!success) {
                    jbiMex.setStatus(ExchangeStatus.ERROR);
                    if (err != null && jbiMex.getError() == null)
                        jbiMex.setError(err);
                } else {
                    if (jbiMex.getStatus() == ExchangeStatus.ACTIVE)
                        jbiMex.setStatus(ExchangeStatus.DONE);
                }
                _ode.getChannel().send(jbiMex);
            }
        } else if (jbiMex.getPattern().equals(org.apache.ode.jbi.MessageExchangePattern.IN_OUT)) {
            boolean success = false;
            Exception err = null;
            try {
                invokeOde(jbiMex, ((InOut) jbiMex).getInMessage());
                success = true;
            } catch (Exception ex) {
                __log.error("Error invoking ODE.", ex);
                err = ex;
            } catch (Throwable t) {
                __log.error("Unexpected error invoking ODE.", t);
                err = new RuntimeException(t);
            } finally {
                // If we got an error that wasn't sent.  
                if (jbiMex.getStatus() == ExchangeStatus.ACTIVE && !success) {
                    if (err != null && jbiMex.getError() == null)  {
                        jbiMex.setError(err);
                    }
                    jbiMex.setStatus(ExchangeStatus.ERROR);     
                    _ode.getChannel().send(jbiMex);         
                }       
            }
        } else {
            __log.error("JBI MessageExchange " + jbiMex.getExchangeId() + " is of an unsupported pattern "
                    + jbiMex.getPattern());
            jbiMex.setStatus(ExchangeStatus.ERROR);
            jbiMex.setError(new Exception("Unknown message exchange pattern: " + jbiMex.getPattern()));
        }

    }

    /**
     * Called from
     * {@link MessageExchangeContextImpl#onAsyncReply(MyRoleMessageExchange)}
     * 
     * @param mex
     *            message exchange
     */
    public void onResponse(MyRoleMessageExchange mex) {
        __log.debug("Consuming MEX tracker " + mex.getClientId());
        javax.jbi.messaging.MessageExchange jbiMex = _jbiMexTracker.consume(mex.getClientId());
        if (jbiMex == null) {
            __log.warn("Ignoring unknown async reply: " + mex);
            return;
        }

        switch (mex.getStatus()) {
        case FAULT:
            outResponseFault(mex, jbiMex);
            break;
        case RESPONSE:
            outResponse(mex, jbiMex);
            break;
        case FAILURE:
            outFailure(mex, jbiMex);
            break;
        default:
            __log.warn("Received ODE message exchange in unexpected state: " + mex.getStatus());
        }
        mex.release();
    }

    /**
     * Forward a JBI input message to ODE.
     * 
     * @param jbiMex
     */
    private void invokeOde(javax.jbi.messaging.MessageExchange jbiMex, NormalizedMessage request) throws Exception {

        // If this has already been tracked, we will not invoke!
        if (_jbiMexTracker.track(jbiMex)) {
            __log.debug("Skipping JBI MEX " + jbiMex.getExchangeId() + ", already received!");
            return;
        }

        _ode.getTransactionManager().begin();

        boolean success = false;
        MyRoleMessageExchange odeMex = null;
        try {
            if (__log.isDebugEnabled()) {
                __log.debug("invokeOde() JBI exchangeId=" + jbiMex.getExchangeId() + " endpoint=" + _endpoint
                        + " operation=" + jbiMex.getOperation());
            }
            odeMex = _ode._server.getEngine().createMessageExchange(jbiMex.getExchangeId(), _endpoint.serviceName,
                    jbiMex.getOperation().getLocalPart());

            if (odeMex.getOperation() != null) {
                copyMexProperties(odeMex, jbiMex);
                javax.wsdl.Message msgdef = odeMex.getOperation().getInput().getMessage();
                Message odeRequest = odeMex.createMessage(odeMex.getOperation().getInput().getMessage().getQName());
                Mapper mapper = _ode.findMapper(request, odeMex.getOperation());
                if (mapper == null) {
                    String errmsg = "Could not find a mapper for request message for JBI MEX " + jbiMex.getExchangeId()
                            + "; ODE MEX " + odeMex.getMessageExchangeId() + " is failed. ";
                    __log.error(errmsg);
                    throw new MessageTranslationException(errmsg);

                }
                odeMex.setProperty(Mapper.class.getName(), mapper.getClass().getName());
                mapper.toODE(odeRequest, request, msgdef);
                odeMex.invoke(odeRequest);

                // Handle the response if it is immediately available.
                if (odeMex.getStatus() != Status.ASYNC) {
                    __log.debug("ODE MEX " + odeMex + " completed SYNCHRONOUSLY.");
                    onResponse(odeMex);
                    _jbiMexTracker.consume(jbiMex.getExchangeId());
                } else {
                    __log.debug("ODE MEX " + odeMex + " completed ASYNCHRONOUSLY.");
                }
            } else {
                __log.error("ODE MEX " + odeMex + " was unroutable.");
                sendError(jbiMex, new IllegalArgumentException("Unroutable invocation."));
            }

            success = true;
            // For one-way invocation we do not need to maintain the association
            if (jbiMex.getPattern().equals(org.apache.ode.jbi.MessageExchangePattern.IN_ONLY)) {
                __log.debug("Consuming non Req/Res MEX tracker " + jbiMex.getExchangeId() + " with pattern " + jbiMex.getPattern());
                _jbiMexTracker.consume(jbiMex.getExchangeId());
            }

        } finally {
            if (success) {
                __log.debug("Commiting ODE MEX " + odeMex);
                _ode.getTransactionManager().commit();
            } else {
                __log.debug("Rolling back ODE MEX " + odeMex);
                _jbiMexTracker.consume(jbiMex.getExchangeId());
                _ode.getTransactionManager().rollback();

            }
        }

    }

    private void outFailure(MyRoleMessageExchange odeMex, javax.jbi.messaging.MessageExchange jbiMex) {
        try {
            jbiMex.setError(new Exception("MEXFailure"));
            jbiMex.setStatus(ExchangeStatus.ERROR);
            // TODO: get failure codes out of the message.
            _ode.getChannel().send(jbiMex);
        } catch (MessagingException ex) {
            __log.fatal("Error bridging ODE out response: ", ex);
        }
    }

    private void outResponse(MyRoleMessageExchange mex, javax.jbi.messaging.MessageExchange jbiMex) {
        InOut inout = (InOut) jbiMex;

        try {
            NormalizedMessage nmsg = inout.createMessage();
            String mapperName = mex.getProperty(Mapper.class.getName());
            Mapper mapper = _ode.getMapper(mapperName);
            if (mapper == null) {
                String errmsg = "Message-mapper " + mapperName + " used in ODE MEX " + mex.getMessageExchangeId()
                        + " is no longer available.";
                __log.error(errmsg);
                throw new MessageTranslationException(errmsg);
            }

            mapper.toNMS(nmsg, mex.getResponse(), mex.getOperation().getOutput().getMessage(), null);

            inout.setOutMessage(nmsg);
            _ode.getChannel().send(inout);

        } catch (MessagingException ex) {
            __log.error("Error bridging ODE out response: ", ex);
            sendError(jbiMex, ex);
        } catch (MessageTranslationException e) {
            __log.error("Error translating ODE message " + mex.getResponse() + " to NMS format!", e);
            sendError(jbiMex, e);
        }
    }

    private void outResponseFault(MyRoleMessageExchange mex, javax.jbi.messaging.MessageExchange jbiMex) {

        InOut inout = (InOut) jbiMex;

        try {
            Fault flt = inout.createFault();
            String mapperName = mex.getProperty(Mapper.class.getName());
            Mapper mapper = _ode.getMapper(mapperName);
            if (mapper == null) {
                String errmsg = "Message-mapper " + mapperName + " used in ODE MEX " + mex.getMessageExchangeId()
                        + " is no longer available.";
                __log.error(errmsg);
                throw new MessageTranslationException(errmsg);
            }

            QName fault = mex.getFault();
            javax.wsdl.Fault wsdlFault = mex.getOperation().getFault(fault.getLocalPart());
            if (wsdlFault == null) {
               sendError(jbiMex, new MessageTranslationException("Unmapped Fault : " + fault + ": " + mex.getFaultExplanation()));
            } else {
                mapper.toNMS(flt, mex.getFaultResponse(), wsdlFault.getMessage(), fault);
                inout.setFault(flt);
                _ode.getChannel().send(inout);
            }
        } catch (MessagingException e) {
            __log.error("Error bridging ODE fault response: ", e);
            sendError(jbiMex, e);
        } catch (MessageTranslationException mte) {
            __log.error("Error translating ODE fault message " + mex.getFaultResponse() + " to NMS format!", mte);
            sendError(jbiMex, mte);
        }
    }
    
    private void sendError(javax.jbi.messaging.MessageExchange jbiMex, Exception error) {
        try {
            jbiMex.setError(error);
            jbiMex.setStatus(ExchangeStatus.ERROR);
            _ode.getChannel().send(jbiMex);
        } catch (Exception e) {
            __log.error("Error sending ERROR status: ", e);
        }
    }

    public Endpoint getEndpoint() {
        return _endpoint;
    }

    /**
     * Class for tracking outstanding message exchanges from JBI.
     */
    private static class JbiMexTracker {
        /**
         * Outstanding JBI-initiated exchanges: mapping for JBI MEX ID to JBI
         * MEX
         */
        private Map<String, javax.jbi.messaging.MessageExchange> _outstandingJbiExchanges = new HashMap<String, javax.jbi.messaging.MessageExchange>();

        synchronized boolean track(javax.jbi.messaging.MessageExchange jbiMex) {
            boolean found = _outstandingJbiExchanges.containsKey(jbiMex.getExchangeId());
            _outstandingJbiExchanges.put(jbiMex.getExchangeId(), jbiMex);
            return found;
        }

        synchronized javax.jbi.messaging.MessageExchange consume(String clientId) {
            return _outstandingJbiExchanges.remove(clientId);
        }

    }
}
