/*
 * Jitsi, the OpenSource Java VoIP and Instant Messaging client.
 *
 * Copyright @ 2015 Atlassian Pty Ltd
 *
 * 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 net.java.sip.communicator.impl.protocol.sip.dtmf;

import gov.nist.javax.sip.header.*;

import java.io.*;
import java.text.*;
import java.util.*;

import javax.sip.*;
import javax.sip.header.*;
import javax.sip.message.*;

import net.java.sip.communicator.impl.protocol.sip.*;
import net.java.sip.communicator.service.protocol.*;
import net.java.sip.communicator.service.protocol.OperationFailedException;
import net.java.sip.communicator.service.protocol.event.*;
import net.java.sip.communicator.util.*;

import org.jitsi.service.neomedia.*;

/**
 * Sending DTMFs with SIP INFO.
 *
 * @author Damian Minkov
 * @author Lyubomir Marinov
 */
public class DTMFInfo
    extends MethodProcessorAdapter
{
    /**
     * The <tt>Logger</tt> used by the <tt>DTMFInfo</tt> class and its instances
     * for logging output.
     */
    private static final Logger logger
        = Logger.getLogger(DTMFInfo.class);

    /**
     * The sub-type of the content of the <tt>Request</tt>s being sent by
     * <tt>DTMFInfo</tt>.
     */
    private static final String CONTENT_SUB_TYPE = "dtmf-relay";

    /**
     * The type of the content of the <tt>Request</tt>s being sent by
     * <tt>DTMFInfo</tt>.
     */
    private static final String CONTENT_TYPE = "application";

    /**
     * Maps call peers and tone and its start time, so we can compute duration.
     */
    private Hashtable<CallPeer, Object[]>
        currentlyTransmittingTones = new Hashtable<CallPeer, Object[]>();

    /**
     * Involved protocol provider service.
     */
    private final ProtocolProviderServiceSipImpl pps;

    /**
     * A list of listeners registered for dtmf tone events.
     */
    private final List<DTMFListener> dtmfListeners =
        new LinkedList<DTMFListener>();

    /**
     * Constructor
     *
     * @param pps the SIP Protocol provider service
     */
    public DTMFInfo(ProtocolProviderServiceSipImpl pps)
    {
        this.pps = pps;

        this.pps.registerMethodProcessor(Request.INFO, this);
    }

    /**
     * Saves the tone we need to send and its start time. With start time we
     * can compute the duration later when we need to send the DTMF.
     *
     * @param callPeer the call peer.
     * @param tone the tone to transmit.
     * @throws OperationFailedException
     * @throws NullPointerException
     * @throws IllegalArgumentException
     */
    public void startSendingDTMF(CallPeerSipImpl callPeer, DTMFTone tone)
        throws OperationFailedException,
                NullPointerException,
                IllegalArgumentException
    {
        if(currentlyTransmittingTones.contains(callPeer))
            throw new IllegalStateException(
                "Error starting dtmf tone, already started");

        currentlyTransmittingTones.put(callPeer,
            new Object[]{tone, System.currentTimeMillis()});
    }

    /**
     * Sending of the currently saved tone.
     * @param callPeer
     */
    public void stopSendingDTMF(CallPeerSipImpl callPeer)
    {
        Object[] toneInfo =
            currentlyTransmittingTones.remove(callPeer);

        if(toneInfo != null)
        {
            try
            {
                long startTime = (Long)toneInfo[1];
                sayInfo(callPeer,
                    (DTMFTone) toneInfo[0],
                     System.currentTimeMillis() - startTime);
            } catch (OperationFailedException ex)
            {
                logger.error("Error stoping dtmf ");
            }
        }
    }

    /**
     * This is just a copy of the bye method from the OpSetBasicTelephony,
     * which was enhanced with a body in order to send the DTMF tone
     *
     * @param callPeer destination of the DTMF tone
     * @param dtmftone DTMF tone to send
     * @param duration the duration of the tone
     * @throws OperationFailedException
     */
    private void sayInfo(CallPeerSipImpl callPeer,
                         DTMFTone dtmftone, long duration)
        throws OperationFailedException
    {
        Request info = pps.getMessageFactory().createRequest(
                        callPeer.getDialog(), Request.INFO);

        //here we add the body
        ContentType ct = new ContentType(CONTENT_TYPE, CONTENT_SUB_TYPE);
        String content
            = "Signal=" + dtmftone.getValue()
                + "\r\nDuration=" + duration + "\r\n";

        ContentLength cl = new ContentLength(content.length());
        info.setContentLength(cl);

        try
        {
            info.setContent(content.getBytes(), ct);
        }
        catch (ParseException ex)
        {
            logger.error("Failed to construct the INFO request", ex);
            throw new OperationFailedException(
                "Failed to construct a client the INFO request"
                , OperationFailedException.INTERNAL_ERROR
                , ex);

        }
        //body ended
        ClientTransaction clientTransaction = null;
        try
        {
            clientTransaction = callPeer.getJainSipProvider()
                .getNewClientTransaction(info);
        }
        catch (TransactionUnavailableException ex)
        {
            logger.error(
                "Failed to construct a client transaction from the INFO request"
                , ex);
            throw new OperationFailedException(
                "Failed to construct a client transaction from the INFO request"
                , OperationFailedException.INTERNAL_ERROR
                , ex);
        }

        try
        {
            if (callPeer.getDialog().getState()
                == DialogState.TERMINATED)
            {
                //this is probably because the call has just ended, so don't
                //throw an exception. simply log and get lost.
                logger.warn("Trying to send a dtmf tone inside a "
                            +"TERMINATED dialog.");
                return;
            }

            callPeer.getDialog().sendRequest(clientTransaction);
            if (logger.isDebugEnabled())
                logger.debug("sent request:\n" + info);
        }
        catch (SipException ex)
        {
            throw new OperationFailedException(
                "Failed to send the INFO request"
                , OperationFailedException.NETWORK_FAILURE
                , ex);
        }
    }

    /**
     * Just look if the DTMF signal was well received, and log it
     *
     * @param responseEvent the response event
     * @return <tt>true</tt> if the specified event has been handled by this
     *         processor and shouldn't be offered to other processors registered
     *         for the same method; <tt>false</tt>, otherwise
     */
    @Override
    public boolean processResponse(ResponseEvent responseEvent)
    {
        boolean processed = false;

        if (responseEvent == null)
        {
            if (logger.isDebugEnabled())
                logger.debug("null responseEvent");
        }
        else
        {
            Response response = responseEvent.getResponse();

            if (response == null)
            {
                if (logger.isDebugEnabled())
                    logger.debug("null response");
            }
            else
            {
                // Is it even for us?
                ClientTransaction clientTransaction
                    = responseEvent.getClientTransaction();

                if (clientTransaction == null)
                {
                    if (logger.isDebugEnabled())
                        logger.debug("null clientTransaction");
                }
                else
                {
                    Request request = clientTransaction.getRequest();

                    if (request == null)
                    {
                        if (logger.isDebugEnabled())
                            logger.debug("null request");
                    }
                    else
                    {
                        ContentTypeHeader contentTypeHeader
                            = (ContentTypeHeader)
                                request.getHeader(ContentTypeHeader.NAME);

                        if ((contentTypeHeader != null)
                                && CONTENT_TYPE.equalsIgnoreCase(
                                        contentTypeHeader.getContentType())
                                && CONTENT_SUB_TYPE.equalsIgnoreCase(
                                        contentTypeHeader.getContentSubType()))
                        {
                            processed = true;

                            int statusCode = response.getStatusCode();

                            if (statusCode == 200)
                            {
                                if (logger.isDebugEnabled())
                                    logger.debug(
                                            "DTMF send succeeded: "
                                                + statusCode);
                            }
                            else
                                logger.error("DTMF send failed: " + statusCode);
                        }
                    }
                }
            }
        }
        return processed;
    }

    /**
     * Receives dtmf info requests.
     */
    @Override
    public boolean processRequest(RequestEvent requestEvent)
    {
        Request request = requestEvent.getRequest();

        ContentTypeHeader contentTypeHeader
            = (ContentTypeHeader)
                request.getHeader(ContentTypeHeader.NAME);

        if ((contentTypeHeader != null)
                && CONTENT_TYPE.equalsIgnoreCase(
                        contentTypeHeader.getContentType())
                && CONTENT_SUB_TYPE.equalsIgnoreCase(
                        contentTypeHeader.getContentSubType()))
        {
            try
            {
                byte[] value;
                Object valueObj = request.getContent();

                if(valueObj instanceof String)
                    value = ((String)valueObj).getBytes("UTF-8");
                else if(valueObj instanceof byte[])
                    value = (byte[])valueObj;
                else
                {
                    logger.error("Unknown content type");
                    return false;
                }

                Properties prop = new Properties();
                prop.load(new ByteArrayInputStream(value));

                String signal = prop.getProperty("Signal");
                String durationStr = prop.getProperty("Duration");

                DTMFTone tone = DTMFTone.getDTMFTone(signal);

                if(tone == null)
                {
                    logger.warn("Unknown tone received: " + tone);
                    return false;
                }

                long duration = 0;
                try
                {
                    duration = Long.parseLong(durationStr);
                }
                catch(NumberFormatException ex)
                {
                    logger.warn("Error parsing duration:" + durationStr, ex);
                }

                // fire event
                fireToneEvent(tone, duration);
            }
            catch(IOException ioe)
            {}

            Response responseOK;

            try
            {
                responseOK = pps.getMessageFactory().createResponse(
                            Response.OK, requestEvent.getRequest());
            }
            catch (ParseException ex)
            {
                //What else could we do apart from logging?
                logger.warn("Failed to create OK for incoming INFO request", ex);
                return false;
            }

            try
            {
                SipStackSharing.getOrCreateServerTransaction(requestEvent).
                    sendResponse(responseOK);
            }
            catch(TransactionUnavailableException ex)
            {
                if (logger.isInfoEnabled())
                    logger.info("Failed to respond to an incoming "
                            +"transactionless INFO request");
                if (logger.isTraceEnabled())
                    logger.trace("Exception was:", ex);
                return false;
            }
            catch (InvalidArgumentException ex)
            {
                //What else could we do apart from logging?
                logger.warn("Failed to send OK for incoming INFO request", ex);
                return false;
            }
            catch (SipException ex)
            {
                //What else could we do apart from logging?
                logger.warn("Failed to send OK for incoming INFO request", ex);
                return false;
            }

            return true;
        }

        return false;
    }

    /**
     * Fire event to interested listeners.
     * @param tone to go into event.
     * @param duration of the tone.
     */
    private void fireToneEvent(DTMFTone tone, long duration)
    {
        Collection<DTMFListener> listeners;
        synchronized (this.dtmfListeners)
        {
            listeners = new ArrayList<DTMFListener>(this.dtmfListeners);
        }

        DTMFReceivedEvent evt = new DTMFReceivedEvent(pps, tone, duration);

        if (logger.isDebugEnabled())
            logger.debug("Dispatching DTMFTone Listeners=" + listeners.size()
            + " evt=" + evt);

        try
        {
            for (DTMFListener listener : listeners)
            {
                listener.toneReceived(evt);
            }
        }
        catch (Throwable e)
        {
            logger.error("Error delivering dtmf tone", e);
        }
    }

    /**
     * Registers the specified DTMFListener with this provider so that it could
     * be notified when incoming DTMF tone is received.
     * @param listener the listener to register with this provider.
     *
     */
    public void addDTMFListener(DTMFListener listener)
    {
        synchronized (dtmfListeners)
        {
            if (!dtmfListeners.contains(listener))
            {
                dtmfListeners.add(listener);
            }
        }
    }

    /**
     * Removes the specified listener from the list of DTMF listeners.
     * @param listener the listener to unregister.
     */
    public void removeDTMFListener(DTMFListener listener)
    {
        synchronized (dtmfListeners)
        {
            dtmfListeners.remove(listener);
        }
    }
}
