/**
 * 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.cxf.ws.rm;

import java.math.BigInteger;
import java.util.Collections;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.xml.datatype.Duration;

import org.apache.cxf.Bus;
import org.apache.cxf.binding.Binding;
import org.apache.cxf.common.logging.LogUtils;
import org.apache.cxf.endpoint.Endpoint;
import org.apache.cxf.interceptor.Fault;
import org.apache.cxf.jaxb.DatatypeFactory;
import org.apache.cxf.message.Exchange;
import org.apache.cxf.message.Message;
import org.apache.cxf.service.invoker.Invoker;
import org.apache.cxf.service.model.OperationInfo;
import org.apache.cxf.ws.addressing.AddressingProperties;
import org.apache.cxf.ws.addressing.VersionTransformer;
import org.apache.cxf.ws.addressing.v200408.AttributedURI;
import org.apache.cxf.ws.rm.manager.DestinationPolicyType;

/**
 * 
 */
public class Servant implements Invoker {

    private static final Logger LOG = LogUtils.getL7dLogger(Servant.class);
    private RMEndpoint reliableEndpoint;
    // REVISIT assumption there is only a single outstanding unattached Identifier
    private Identifier unattachedIdentifier;
 
    Servant(RMEndpoint rme) {
        reliableEndpoint = rme;
    }
    
    private void throwSequenceFault(SequenceFault sf, Exchange exchange) {
        Endpoint e = exchange.get(Endpoint.class);
        Binding b = null;
        if (null != e) {
            b = e.getBinding();
        }
        Bus bus = exchange.get(Bus.class);
        if (null != b && bus != null) {
            RMManager m = bus.getExtension(RMManager.class);
            LOG.fine("Manager: " + m);
            BindingFaultFactory bff = m.getBindingFaultFactory(b);
            Fault f = bff.createFault(sf);
            LogUtils.log(LOG, Level.SEVERE, "SEQ_FAULT_MSG", bff.toString(f));
            throw f;
        }
        throw new Fault(sf);
    }
    
    
    public Object invoke(Exchange exchange, Object o) {
        LOG.fine("Invoking on RM Endpoint");
        OperationInfo oi = exchange.get(OperationInfo.class);
        if (null == oi) {
            LOG.fine("No operation info."); 
            return null;
        }
        
        if (RMConstants.getCreateSequenceOperationName().equals(oi.getName())
            || RMConstants.getCreateSequenceOnewayOperationName().equals(oi.getName())) {
            try {
                return Collections.singletonList(createSequence(exchange.getInMessage()));
            } catch (SequenceFault ex) {
                throwSequenceFault(ex, exchange);
            } catch (Exception ex) {
                throw new Fault(ex);
            }
        } else if (RMConstants.getCreateSequenceResponseOnewayOperationName().equals(oi.getName())) {
            CreateSequenceResponseType createResponse = 
                (CreateSequenceResponseType)getParameter(exchange.getInMessage());
            try {
                createSequenceResponse(createResponse);
            } catch (SequenceFault ex) {
                throwSequenceFault(ex, exchange);
            }
        } else if (RMConstants.getTerminateSequenceOperationName().equals(oi.getName())) {            
            try {
                terminateSequence(exchange.getInMessage());
            } catch (SequenceFault ex) {
                throwSequenceFault(ex, exchange);
            } catch (RMException ex) {
                throw new Fault(ex);
            }
        }
        
        return null;
    }


    CreateSequenceResponseType createSequence(Message message) throws SequenceFault {
        LOG.fine("Creating sequence");
        
        AddressingProperties maps = RMContextUtils.retrieveMAPs(message, false, false);        
        Message outMessage = message.getExchange().getOutMessage();  
        if (null != outMessage) {
            RMContextUtils.storeMAPs(maps, outMessage, false, false);
        }
        
        CreateSequenceType create = (CreateSequenceType)getParameter(message);
        Destination destination = reliableEndpoint.getDestination();
        
        CreateSequenceResponseType createResponse = 
            RMUtils.getWSRMFactory().createCreateSequenceResponseType();        
        createResponse.setIdentifier(destination.generateSequenceIdentifier());
        
        DestinationPolicyType dp = reliableEndpoint.getManager().getDestinationPolicy();
        Duration supportedDuration = dp.getSequenceExpiration();
        if (null == supportedDuration) {
            supportedDuration = DatatypeFactory.PT0S;
        }
        Expires ex = create.getExpires();
        
        if (null != ex) {
            Duration effectiveDuration = ex.getValue();
            // PT0S represents 0 second and the shortest duration but in ws-rm, considered the longest 
            if (DatatypeFactory.PT0S.equals(effectiveDuration) 
                || (!DatatypeFactory.PT0S.equals(supportedDuration) 
                    &&  supportedDuration.isShorterThan(effectiveDuration)))  {
                effectiveDuration = supportedDuration;
            }
            ex = RMUtils.getWSRMFactory().createExpires();
            ex.setValue(effectiveDuration);
            createResponse.setExpires(ex);
        }
        
        OfferType offer = create.getOffer();
        if (null != offer) {
            AcceptType accept = RMUtils.getWSRMFactory().createAcceptType();
            if (dp.isAcceptOffers()) {
                Source source = reliableEndpoint.getSource();
                LOG.fine("Accepting inbound sequence offer");
                // AddressingProperties maps = RMContextUtils.retrieveMAPs(message, false, false);
                AttributedURI to = VersionTransformer.convert(maps.getTo());
                accept.setAcksTo(RMUtils.createReference2004(to.getValue()));
                SourceSequence seq = new SourceSequence(offer.getIdentifier(), 
                                                                    null, 
                                                                    createResponse.getIdentifier());
                seq.setExpires(offer.getExpires());
                seq.setTarget(VersionTransformer.convert(create.getAcksTo()));
                source.addSequence(seq);
                source.setCurrent(createResponse.getIdentifier(), seq);  
                if (LOG.isLoggable(Level.FINE)) {
                    LOG.fine("Making offered sequence the current sequence for responses to "
                             + createResponse.getIdentifier().getValue());
                }
            } else {
                if (LOG.isLoggable(Level.FINE)) {
                    LOG.fine("Refusing inbound sequence offer"); 
                }
                accept.setAcksTo(RMUtils.createNoneReference2004());
            }
            createResponse.setAccept(accept);
        }
        
        DestinationSequence seq = new DestinationSequence(createResponse.getIdentifier(),
                                                          create.getAcksTo(), destination);
        seq.setCorrelationID(maps.getMessageID().getValue());
        destination.addSequence(seq);
        LOG.fine("returning " + createResponse);
        return createResponse;
    }

    public void createSequenceResponse(CreateSequenceResponseType createResponse) throws SequenceFault {
        LOG.fine("Creating sequence response");
        
        SourceSequence seq = new SourceSequence(createResponse.getIdentifier());
        seq.setExpires(createResponse.getExpires());
        Source source  = reliableEndpoint.getSource();
        source.addSequence(seq);
        
        // the incoming sequence ID is either used as the requestor sequence
        // (signalled by null) or associated with a corresponding sequence 
        // identifier
        source.setCurrent(clearUnattachedIdentifier(), seq);

        // if a sequence was offered and accepted, then we can add this to
        // to the local destination sequence list, otherwise we have to wait for
        // and incoming CreateSequence request
        
        Identifier offeredId = reliableEndpoint.getProxy().getOfferedIdentifier();
        if (null != offeredId) {
            AcceptType accept = createResponse.getAccept();
            assert null != accept;
            Destination dest = reliableEndpoint.getDestination();
            String address = accept.getAcksTo().getAddress().getValue();
            if (!RMUtils.getAddressingConstants().getNoneURI().equals(address)) {
                DestinationSequence ds = 
                    new DestinationSequence(offeredId, accept.getAcksTo(), dest);
                dest.addSequence(ds);
            }
        }
    }

    public void terminateSequence(Message message) throws SequenceFault, RMException {
        LOG.fine("Terminating sequence");
        
        TerminateSequenceType terminate = (TerminateSequenceType)getParameter(message);
        
        // check if the terminated sequence was created in response to a a createSequence
        // request
        
        Destination destination = reliableEndpoint.getDestination();
        Identifier sid = terminate.getIdentifier();
        DestinationSequence terminatedSeq = destination.getSequence(sid);
        if (null == terminatedSeq) {
            //  TODO
            LOG.severe("No such sequence.");
            return;
        } 

        destination.removeSequence(terminatedSeq);
        
        // the following may be necessary if the last message for this sequence was a oneway
        // request and hence there was no response to which a last message could have been added
        
        // REVISIT: A last message for the correlated sequence should have been sent by the time
        // the last message for the underlying sequence was received.
        
        Source source = reliableEndpoint.getSource();
        
        for (SourceSequence outboundSeq : source.getAllSequences()) {
            if (outboundSeq.offeredBy(sid) && !outboundSeq.isLastMessage()) {
                
                if (BigInteger.ZERO.equals(outboundSeq.getCurrentMessageNr())) {
                    source.removeSequence(outboundSeq);
                }
                // send an out of band message with an empty body and a 
                // sequence header containing a lastMessage element.
               
                /*
                Proxy proxy = new Proxy(reliableEndpoint);
                try {
                    proxy.lastMessage(outboundSeq);
                } catch (RMException ex) {
                    LogUtils.log(LOG, Level.SEVERE, "CORRELATED_SEQ_TERMINATION_EXC", ex);
                }
                */
                
                break;
            }
        }
        
    }

    Object getParameter(Message message) {
        List resList = null;
        // assert message == message.getExchange().getInMessage();
        
        if (message != null) {
            resList = message.getContent(List.class);
        }

        if (resList != null) {
            return resList.get(0);
        }
        return null;
    }
    
    Identifier clearUnattachedIdentifier() {
        Identifier ret = unattachedIdentifier;
        unattachedIdentifier = null;
        return ret;
    }
    
    void setUnattachedIdentifier(Identifier i) { 
        unattachedIdentifier = i;
    }
}
