/**
 * 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.sip.listener;

import java.text.ParseException;
import java.util.UUID;

import javax.sip.ClientTransaction;
import javax.sip.Dialog;
import javax.sip.DialogTerminatedEvent;
import javax.sip.IOExceptionEvent;
import javax.sip.RequestEvent;
import javax.sip.ResponseEvent;
import javax.sip.ServerTransaction;
import javax.sip.SipException;
import javax.sip.SipListener;
import javax.sip.SipProvider;
import javax.sip.TransactionTerminatedEvent;
import javax.sip.address.SipURI;
import javax.sip.header.EventHeader;
import javax.sip.header.SubscriptionStateHeader;
import javax.sip.header.ToHeader;
import javax.sip.message.Request;
import javax.sip.message.Response;

import org.apache.camel.component.sip.SipPresenceAgent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class SipPresenceAgentListener implements SipListener, SipMessageCodes {
    private static final transient Logger LOG = LoggerFactory.getLogger(SipPresenceAgentListener.class);
    protected Dialog dialog;
    protected int notifyCount;
    private SipPresenceAgent sipPresenceAgent;

    public SipPresenceAgentListener(SipPresenceAgent sipPresenceAgent) {
        this.sipPresenceAgent = sipPresenceAgent;
    }

    public void processRequest(RequestEvent requestEvent) {
        Request request = requestEvent.getRequest();
        ServerTransaction serverTransactionId = requestEvent.getServerTransaction();

        if (LOG.isDebugEnabled()) {
            LOG.debug("Request: " + request.getMethod()); 
            LOG.debug("Server Transaction Id:" + serverTransactionId);
        }

        if (request.getMethod().equals(Request.SUBSCRIBE)) {
            processSubscribe(requestEvent, serverTransactionId);
        } else if (request.getMethod().equals(Request.PUBLISH)) {
            processPublish(requestEvent, serverTransactionId);
        } else {
            if (LOG.isDebugEnabled()) {
                LOG.debug("Received expected request with method: " + request.getMethod() + ". No further processing done");
            }
        }
    }
    
    private void sendNotification(EventHeader eventHeader, boolean isInitial, Object body) throws SipException, ParseException {
        /*
         * NOTIFY requests MUST contain a "Subscription-State" header with a
         * value of "active", "pending", or "terminated". The "active" value
         * indicates that the subscription has been accepted and has been
         * authorized (in most cases; see section 5.2.). The "pending" value
         * indicates that the subscription has been received, but that
         * policy information is insufficient to accept or deny the
         * subscription at this time. The "terminated" value indicates that
         * the subscription is not active.
         */
        
        Request notifyRequest = dialog.createRequest("NOTIFY");

        // Mark the contact header, to check that the remote contact is updated
        ((SipURI)sipPresenceAgent.getConfiguration().getContactHeader().getAddress().getURI()).setParameter(
                sipPresenceAgent.getConfiguration().getFromUser(), sipPresenceAgent.getConfiguration().getFromHost());

        SubscriptionStateHeader sstate;
        if (isInitial) {
            // Initial state is pending, second time we assume terminated (Expires==0)
            sstate = 
                sipPresenceAgent.getConfiguration().getHeaderFactory().createSubscriptionStateHeader(isInitial ? SubscriptionStateHeader.PENDING : SubscriptionStateHeader.TERMINATED);
    
            // Need a reason for terminated
            if (sstate.getState().equalsIgnoreCase("terminated")) {
                sstate.setReasonCode("deactivated");
            }
        } else {
            sstate = sipPresenceAgent.getConfiguration().getHeaderFactory().createSubscriptionStateHeader(SubscriptionStateHeader.ACTIVE);
        }

        notifyRequest.addHeader(sstate);
        notifyRequest.setHeader(eventHeader);
        notifyRequest.setHeader(sipPresenceAgent.getConfiguration().getContactHeader());
        notifyRequest.setContent(body, sipPresenceAgent.getConfiguration().getContentTypeHeader());
        if (LOG.isDebugEnabled()) {
            LOG.debug("Sending the following NOTIFY request to Subscriber: " + notifyRequest);
        }
        
        ClientTransaction clientTransactionId = sipPresenceAgent.getProvider().getNewClientTransaction(notifyRequest);

        dialog.sendRequest(clientTransactionId);
    }
    
    private void processPublish(RequestEvent requestEvent,
            ServerTransaction serverTransactionId) {
        try {
            Request request = requestEvent.getRequest();
            if (LOG.isDebugEnabled()) {
                LOG.debug("SipPresenceAgentListener: Received a Publish request, sending OK");
                LOG.debug("SipPresenceAgentListener request: " + request);
            }
            EventHeader eventHeader = (EventHeader) requestEvent.getRequest().getHeader(EventHeader.NAME);
            Response response = sipPresenceAgent.getConfiguration().getMessageFactory().createResponse(202, request);
            sipPresenceAgent.getProvider().sendResponse(response);

            // Send notification to subscriber
            sendNotification(eventHeader, false, request.getContent());
                     
        } catch (Exception e) {
            LOG.error("Exception thrown during publish/notify processing in the Sip Presence Agent Listener", e);
        }
    }

    public void processSubscribe(RequestEvent requestEvent,
            ServerTransaction serverTransaction) {
        SipProvider sipProvider = (SipProvider) requestEvent.getSource();
        Request request = requestEvent.getRequest();
        try {
            if (LOG.isDebugEnabled()) {
                LOG.debug("SipPresenceAgentListener: Received a Subscribe request, sending OK");
                LOG.debug("SipPresenceAgentListener request: " + request);
            }
            EventHeader eventHeader = (EventHeader) request.getHeader(EventHeader.NAME);
            if (eventHeader == null) {
                if (LOG.isDebugEnabled()) {
                    LOG.debug("Cannot find event header.... dropping request.");
                }
                return;
            }

            // Always create a ServerTransaction, best as early as possible in the code
            Response response = null;
            ServerTransaction st = requestEvent.getServerTransaction();
            if (st == null) {
                st = sipProvider.getNewServerTransaction(request);
            }

            // Check if it is an initial SUBSCRIBE or a refresh / unsubscribe
            boolean isInitial = requestEvent.getDialog() == null;
            if (isInitial) {
                String toTag = UUID.randomUUID().toString();
                response = sipPresenceAgent.getConfiguration().getMessageFactory().createResponse(202, request);
                ToHeader toHeader = (ToHeader) response.getHeader(ToHeader.NAME);
                toHeader.setTag(toTag); // Application is supposed to set.

                this.dialog = st.getDialog();
                // subscribe dialogs do not terminate on bye.
                this.dialog.terminateOnBye(false);
            } else {
                response = sipPresenceAgent.getConfiguration().getMessageFactory().createResponse(200, request);
            }

            // Both 2xx response to SUBSCRIBE and NOTIFY need a Contact
            response.addHeader(sipPresenceAgent.getConfiguration().getContactHeader());

            // Expires header is mandatory in 2xx responses to SUBSCRIBE
            response.addHeader(sipPresenceAgent.getConfiguration().getExpiresHeader());
            st.sendResponse(response);
            
            if (LOG.isDebugEnabled()) {
                LOG.debug("SipPresenceAgentListener: Sent OK Message");
                LOG.debug("SipPresenceAgentListener response: " + response);
            }
            sendNotification(eventHeader, isInitial, request.getContent());

        } catch (Throwable e) {
            LOG.error("Exception thrown during Notify processing in the SipPresenceAgentListener.", e);
        }
    }

    public synchronized void processResponse(ResponseEvent responseReceivedEvent) {
        Response response = responseReceivedEvent.getResponse();
        Integer statusCode = response.getStatusCode();
        if (SIP_MESSAGE_CODES.containsKey(statusCode)) {
            LOG.debug(SIP_MESSAGE_CODES.get(statusCode) + " received from Subscriber");
        }
    }

    public void processTimeout(javax.sip.TimeoutEvent timeoutEvent) {
        if (LOG.isWarnEnabled()) {
            LOG.warn("TimeoutEvent received at Sip Subscription Listener");
        }
    }

    public void processIOException(IOExceptionEvent exceptionEvent) {
        if (LOG.isWarnEnabled()) {
            LOG.warn("IOExceptionEvent received at SipPresenceAgentListener");
        }
    }

    public void processTransactionTerminated(
            TransactionTerminatedEvent transactionTerminatedEvent) {
        if (LOG.isWarnEnabled()) {
            LOG.warn("TransactionTerminatedEvent received at SipPresenceAgentListener");
        }
    }

    public void processDialogTerminated(
            DialogTerminatedEvent dialogTerminatedEvent) {
        if (LOG.isWarnEnabled()) {
            LOG.warn("DialogTerminatedEvent received at SipPresenceAgentListener");
        }
    }

}
