/*
 * 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.jabber.jinglesdp;

import java.net.*;
import java.util.*;

import net.java.sip.communicator.impl.protocol.jabber.*;
import org.jitsi.xmpp.extensions.jingle.*;
import org.jitsi.xmpp.extensions.jingle.ContentPacketExtension.*;
import net.java.sip.communicator.service.protocol.media.*;
import net.java.sip.communicator.util.*;
import net.java.sip.communicator.util.Logger;

import org.jitsi.service.neomedia.*;
import org.jitsi.service.neomedia.format.*;
import org.jitsi.utils.*;

/**
 * The class contains a number of utility methods that are meant to facilitate
 * creating and parsing jingle media rtp description descriptions and
 * transports.
 *
 * @author Emil Ivov
 * @author Lyubomir Marinov
 */
public class JingleUtils
{
    /**
     * The <tt>Logger</tt> used by the <tt>JingleUtils</tt> class and its
     * instances for logging output.
     */
    private static final Logger logger = Logger.getLogger(JingleUtils.class);

    /**
     * Extracts and returns an {@link RtpDescriptionPacketExtension} provided
     * with <tt>content</tt> or <tt>null</tt> if there is none.
     *
     * @param content the media content that we'd like to extract the
     * {@link RtpDescriptionPacketExtension} from.
     *
     * @return an {@link RtpDescriptionPacketExtension} provided with
     * <tt>content</tt> or <tt>null</tt> if there is none.
     */
    public static RtpDescriptionPacketExtension getRtpDescription(
                                        ContentPacketExtension content)
    {
        return content.getFirstChildOfType(RtpDescriptionPacketExtension.class);
    }

    /**
     * Extracts and returns the list of <tt>MediaFormat</tt>s advertised in
     * <tt>description</tt> preserving their oder and registering dynamic payload
     * type numbers in the specified <tt>ptRegistry</tt>. Note that this method
     * would only include in the result list <tt>MediaFormat</tt> instances
     * that are currently supported by our <tt>MediaService</tt> implementation
     * and enabled in its configuration. This means that the method could
     * return an empty list even if there were actually some formats in the
     * <tt>mediaDesc</tt> if we support none of them or if all these we support
     * are not enabled in the <tt>MediaService</tt> configuration form.
     *
     * @param description the <tt>MediaDescription</tt> that we'd like to probe
     * for a list of <tt>MediaFormat</tt>s
     * @param ptRegistry a reference to the <tt>DynamycPayloadTypeRegistry</tt>
     * where we should be registering newly added payload type number to format
     * mappings.
     *
     * @return an ordered list of <tt>MediaFormat</tt>s that are both advertised
     * in the <tt>description</tt> and supported by our <tt>MediaService</tt>
     * implementation.
     */
    public static List<MediaFormat> extractFormats(
                                     RtpDescriptionPacketExtension description,
                                     DynamicPayloadTypeRegistry    ptRegistry)
    {
        List<MediaFormat> mediaFmts = new ArrayList<MediaFormat>();

        if (description == null)
        {
            return mediaFmts;
        }

        List<PayloadTypePacketExtension> payloadTypes
                                            = description.getPayloadTypes();

        for(PayloadTypePacketExtension ptExt : payloadTypes)
        {
            MediaFormat format = payloadTypeToMediaFormat(ptExt, ptRegistry);

            //continue if our media service does not know this format
            if(format == null)
            {
                if(logger.isTraceEnabled())
                    logger.trace("Unsupported remote format: " + ptExt.toXML());
            }
            else
                mediaFmts.add(format);
        }

        return mediaFmts;
    }

    /**
     * Returns the {@link MediaFormat} described in the <tt>payloadType</tt>
     * extension or <tt>null</tt> if we don't recognize the format.
     *
     * @param payloadType the {@link PayloadTypePacketExtension} which is to be
     * parsed into a {@link MediaFormat}.
     * @param ptRegistry the {@link DynamicPayloadTypeRegistry} that we would
     * use for the registration of possible dynamic payload types or
     * <tt>null</tt> the returned <tt>MediaFormat</tt> is to not be registered
     * into a <tt>DynamicPayloadTypeRegistry</tt>.
     *
     * @return the {@link MediaFormat} described in the <tt>payloadType</tt>
     * extension or <tt>null</tt> if we don't recognize the format.
     */
    public static MediaFormat payloadTypeToMediaFormat(
            PayloadTypePacketExtension payloadType,
            DynamicPayloadTypeRegistry ptRegistry)
    {
        return
            payloadTypeToMediaFormat(
                    payloadType,
                    JabberActivator.getMediaService(),
                    ptRegistry);
    }

    /**
     * Returns the {@link MediaFormat} described in the <tt>payloadType</tt>
     * extension or <tt>null</tt> if we don't recognize the format.
     *
     * @param payloadType the {@link PayloadTypePacketExtension} which is to be
     * parsed into a {@link MediaFormat}.
     * @param mediaService the <tt>MediaService</tt> implementation which is to
     * be used for <tt>MediaFormat</tt>-related factory methods
     * @param ptRegistry the {@link DynamicPayloadTypeRegistry} that we would
     * use for the registration of possible dynamic payload types or
     * <tt>null</tt> the returned <tt>MediaFormat</tt> is to not be registered
     * into a <tt>DynamicPayloadTypeRegistry</tt>.
     *
     * @return the {@link MediaFormat} described in the <tt>payloadType</tt>
     * extension or <tt>null</tt> if we don't recognize the format.
     */
    public static MediaFormat payloadTypeToMediaFormat(
                    PayloadTypePacketExtension payloadType,
                    MediaService mediaService,
                    DynamicPayloadTypeRegistry ptRegistry)
    {
        byte pt = (byte)payloadType.getID();
        boolean unknown = false;

        //convert params to a name:value map
        List<ParameterPacketExtension> params = payloadType.getParameters();
        Map<String, String> paramsMap = new HashMap<String, String>();
        Map<String, String> advancedMap = new HashMap<String, String>();

        for(ParameterPacketExtension param : params)
        {
            String paramName = param.getName();

            if("imageattr".equals(paramName))
                advancedMap.put(paramName, param.getValue());
            else
                paramsMap.put(paramName, param.getValue());
        }

        for(String attr : payloadType.getAttributeNames())
        {
            //video-related attributes in payload-type element
            if(attr.equals("width") || attr.equals("height"))
                paramsMap.put(attr, payloadType.getAttributeAsString(attr));

            //update ptime with the actual value from the payload
            if (attr.equals(PayloadTypePacketExtension.PTIME_ATTR_NAME))
                advancedMap.put(PayloadTypePacketExtension.PTIME_ATTR_NAME,
                        Integer.toString(payloadType.getPtime()));
        }

        //now create the format.
        MediaFormatFactory formatFactory = mediaService.getFormatFactory();
        MediaFormat format
            = formatFactory.createMediaFormat(
                    pt,
                    payloadType.getName(),
                    payloadType.getClockrate(),
                    payloadType.getChannels(),
                    -1,
                    paramsMap,
                    advancedMap);

        //we don't seem to know anything about this format
        if(format == null)
        {
            unknown = true;
            format = formatFactory.createUnknownMediaFormat(MediaType.AUDIO);
        }

        /*
         * We've just created a MediaFormat for the specified payloadType
         * so we have to remember the mapping between the two so that we
         * don't, for example, map the same payloadType to a different
         * MediaFormat at a later time when we do automatic generation
         * of payloadType in DynamicPayloadTypeRegistry. If the remote peer
         * tries to remap a payloadType in its answer to a different MediaFormat
         * than the one we've specified in our offer, then the dynamic paylaod
         * type registry will keep the original value for receiving and also
         * add an overriding value for the new one. The overriding value will
         * be streamed to our peer.
         */
        if ((ptRegistry != null)
                && (pt >= MediaFormat.MIN_DYNAMIC_PAYLOAD_TYPE)
                && (pt <= MediaFormat.MAX_DYNAMIC_PAYLOAD_TYPE))
                //some systems will violate 3264 by reusing previously defined
                //payload types for new formats. we try and salvage that
                //situation by creating an overriding mapping in such cases
                //we therefore don't do the following check.
                //&& (ptRegistry.findFormat(pt) == null))
        {
            ptRegistry.addMapping(format, pt);
        }

        return unknown ? null : format;
    }

    /**
     * Extracts and returns the list of <tt>RTPExtension</tt>s advertised in
     * <tt>desc</tt> and registers newly encountered ones into the specified
     * <tt>extMap</tt>. The method returns an empty list in case there were no
     * <tt>extmap</tt> advertisements in <tt>desc</tt>.
     *
     * @param desc the {@link RtpDescriptionPacketExtension} that we'd like to
     * probe for a list of {@link RTPExtension}s
     * @param extMap a reference to the <tt>DynamicRTPExtensionsRegistry</tt>
     * where we should be registering newly added extension mappings.
     *
     * @return a <tt>List</tt> of {@link RTPExtension}s advertised in the
     * <tt>mediaDesc</tt> description.
     */
    public static List<RTPExtension> extractRTPExtensions(
                                         RtpDescriptionPacketExtension desc,
                                         DynamicRTPExtensionsRegistry  extMap)
    {
        List<RTPExtension> extensionsList = new ArrayList<RTPExtension>();

        if (desc == null)
        {
            return extensionsList;
        }

        List<RTPHdrExtPacketExtension> extmapList = desc.getExtmapList();

        for (RTPHdrExtPacketExtension extmap : extmapList)
        {
            RTPExtension rtpExtension
                    = new RTPExtension(
                            extmap.getURI(),
                            getDirection(extmap.getSenders(), false),
                            extmap.getAttributes());

            extensionsList.add(rtpExtension);
        }

        return extensionsList;
    }

    /**
     * Converts the specified media <tt>direction</tt> into the corresponding
     * {@link SendersEnum} value so that we could add it to a content element.
     * The <tt>initiatorPerspectice</tt> allows callers to specify whether the
     * direction is to be considered from the session initator's perspective
     * or that of the responder.
     * <p>
     * Example: A {@link MediaDirection#SENDONLY} value would be translated to
     * {@link SendersEnum#initiator} from the initiator's perspective and to
     * {@link SendersEnum#responder} otherwise.
     *
     * @param direction the {@link MediaDirection} that we'd like to translate.
     * @param initiatorPerspective <tt>true</tt> if the <tt>direction</tt> param
     * is to be considered from the initiator's perspective and <tt>false</tt>
     * otherwise.
     *
     * @return one of the <tt>MediaDirection</tt> values indicating the
     * direction of the media steam described by <tt>content</tt>.
     */
    public static SendersEnum getSenders(MediaDirection direction,
                                         boolean   initiatorPerspective)
    {
        if (direction ==  MediaDirection.SENDRECV)
            return SendersEnum.both;
        if (direction ==  MediaDirection.INACTIVE)
            return SendersEnum.none;

        if(initiatorPerspective)
        {
            if(direction == MediaDirection.SENDONLY)
                return SendersEnum.initiator;
            else // recvonly
                return SendersEnum.responder;
        }
        else
        {
            if(direction == MediaDirection.SENDONLY)
                return SendersEnum.responder;
            else // recvonly
                return SendersEnum.initiator;
        }
    }

    /**
     * Determines the direction of the media stream that <tt>content</tt>
     * describes and returns the corresponding <tt>MediaDirection</tt> enum
     * entry. The method looks for a direction specifier attribute (i.e. the
     * content 'senders' attribute) or the absence thereof and returns the
     * corresponding <tt>MediaDirection</tt> entry. The
     * <tt>initiatorPerspectice</tt> allows callers to specify whether the
     * direction is to be considered from the session initiator's perspective
     * or that of the responder.
     * <p>
     * Example: An <tt>initiator</tt> value would be translated to
     * {@link MediaDirection#SENDONLY} from the initiator's perspective and to
     * {@link MediaDirection#RECVONLY} from the responder's.
     *
     * @param content the description of the media stream whose direction
     * we are trying to determine.
     * @param initiatorPerspective <tt>true</tt> if the senders argument is to
     * be translated into a direction from the initiator's perspective and
     * <tt>false</tt> for the sender's.
     *
     * @return one of the <tt>MediaDirection</tt> values indicating the
     * direction of the media steam described by <tt>content</tt>.
     */
    public static MediaDirection getDirection(
                                            ContentPacketExtension content,
                                            boolean   initiatorPerspective)
    {
        SendersEnum senders = content.getSenders();

        return getDirection(senders, initiatorPerspective);
    }

    /**
     * Determines the direction of the media stream that <tt>content</tt>
     * describes and returns the corresponding <tt>MediaDirection</tt> enum
     * entry. The method looks for a direction specifier attribute (i.e. the
     * content 'senders' attribute) or the absence thereof and returns the
     * corresponding <tt>MediaDirection</tt> entry. The
     * <tt>initiatorPerspectice</tt> allows callers to specify whether the
     * direction is to be considered from the session initiator's perspective
     * or that of the responder.
     *
     * @param senders senders direction
     * @param initiatorPerspective <tt>true</tt> if the senders argument is to
     * be translated into a direction from the initiator's perspective and
     * <tt>false</tt> for the sender's.
     *
     * @return one of the <tt>MediaDirection</tt> values indicating the
     * direction of the media steam described by <tt>content</tt>.
     */
    public static MediaDirection getDirection(SendersEnum senders,
            boolean initiatorPerspective)
    {
        if(senders == null)
            return MediaDirection.SENDRECV;

        if (senders == SendersEnum.initiator)
        {
            if(initiatorPerspective)
                return MediaDirection.SENDONLY;
            else
                return MediaDirection.RECVONLY;
        }
        else if (senders == SendersEnum.responder)
        {
            if(initiatorPerspective)
                return MediaDirection.RECVONLY;
            else
                return MediaDirection.SENDONLY;
        }
        else if (senders == SendersEnum.both)
            return MediaDirection.SENDRECV;
        else //if (senders == SendersEnum.none)
            return MediaDirection.INACTIVE;
    }

    /**
     * Returns the default candidate for the specified content <tt>content</tt>.
     * The method is used when establishing new calls and we need a default
     * candidate to initiate our stream with before we've discovered the one
     * that ICE would pick.
     *
     * @param content the stream whose default candidate we are looking for.
     *
     * @return a {@link MediaStreamTarget} containing the default
     * <tt>candidate</tt>s for the stream described in <tt>content</tt> or
     * <tt>null</tt>, if for some reason, the packet does not contain any
     * candidates.
     */
    public static MediaStreamTarget extractDefaultTarget(
            ContentPacketExtension content)
    {
        IceUdpTransportPacketExtension transport
            = content.getFirstChildOfType(IceUdpTransportPacketExtension.class);

        return (transport == null) ? null : extractDefaultTarget(transport);
    }

    public static MediaStreamTarget extractDefaultTarget(
            IceUdpTransportPacketExtension transport)
    {
        //extract the default rtp candidate:
        CandidatePacketExtension rtpCand
            = getFirstCandidate(
                    transport,
                    CandidatePacketExtension.RTP_COMPONENT_ID);

        if (rtpCand == null)
            return null;

        InetAddress rtpAddress = null;

        try
        {
            rtpAddress = NetworkUtils.getInetAddress(rtpCand.getIP());
        }
        catch (UnknownHostException exc)
        {
            throw new IllegalArgumentException(
                    "Failed to parse address " + rtpCand.getIP(),
                    exc);
        }

        //rtp port
        int rtpPort = rtpCand.getPort();
        InetSocketAddress rtpTarget
            = new InetSocketAddress(rtpAddress, rtpPort);

        //extract the RTCP candidate
        CandidatePacketExtension rtcpCand
            = getFirstCandidate(
                    transport,
                    CandidatePacketExtension.RTCP_COMPONENT_ID);
        InetSocketAddress rtcpTarget;

        if( rtcpCand == null)
        {
            rtcpTarget = new InetSocketAddress(rtpAddress, rtpPort + 1);
        }
        else
        {
            InetAddress rtcpAddress = null;

            try
            {
                rtcpAddress = NetworkUtils.getInetAddress(rtcpCand.getIP());
            }
            catch (UnknownHostException exc)
            {
                throw new IllegalArgumentException(
                        "Failed to parse address " + rtcpCand.getIP(),
                        exc);
            }

            //rtcp port
            int rtcpPort = rtcpCand.getPort();

            rtcpTarget = new InetSocketAddress(rtcpAddress, rtcpPort);
        }

        return new MediaStreamTarget(rtpTarget, rtcpTarget);
    }

    /**
     * Returns the first candidate for the specified <tt>componentID</tt> or
     * null if no such component exists.
     *
     * @param content the {@link ContentPacketExtension} that we'll be searching
     * for a component.
     * @param componentID the id of the component that we are looking for
     * (e.g. 1 for RTP, 2 for RTCP);
     *
     * @return the first candidate for the specified <tt>componentID</tt> or
     * null if no such component exists.
     */
    public static CandidatePacketExtension getFirstCandidate(
            ContentPacketExtension content,
            int componentID)
    {
        //passing IceUdp would also return RawUdp transports as one extends
        //the other.
        IceUdpTransportPacketExtension transport
            = content.getFirstChildOfType(IceUdpTransportPacketExtension.class);

        return
            (transport == null)
                ? null
                : getFirstCandidate(transport, componentID);
    }

    public static CandidatePacketExtension getFirstCandidate(
            IceUdpTransportPacketExtension transport,
            int componentID)
    {
        for(CandidatePacketExtension cand : transport.getCandidateList())
        {
            //we don't care about remote candidates!
            if (!(cand instanceof RemoteCandidatePacketExtension)
                    && (cand.getComponent() == componentID))
            {
                return cand;
            }
        }
        return null;
    }

    /**
     * Creates a new {@link ContentPacketExtension} instance according to the
     * specified <tt>formats</tt>, <tt>connector</tt> and <tt>direction</tt>,
     * and using the <tt>dynamicPayloadTypes</tt> registry to handle dynamic
     * payload type registrations. The type (e.g. audio/video) of the media
     * description is determined via from the type of the first
     * {@link MediaFormat} in the <tt>formats</tt> list.
     *
     * @param creator indicates whether the person who originally created this
     * content was the initiator or the responder of the jingle session
     * @param contentName the name of the content element as indicator by the
     * creator or, in case we are the creators: as we'd like it to be.
     * @param formats the list of formats that should be advertised in the newly
     * created content extension.
     * @param senders indicates the direction of the media in this stream.
     * @param rtpExtensions a list of <tt>RTPExtension</tt>s supported by the
     * <tt>MediaDevice</tt> that we will be advertising.
     * @param dynamicPayloadTypes a reference to the
     * <tt>DynamicPayloadTypeRegistry</tt> that we should be using to lookup
     * and register dynamic RTP mappings.
     * @param rtpExtensionsRegistry a reference to the
     * <tt>DynamicRTPExtensionRegistry</tt> that we should be using to lookup
     * and register URN to ID mappings.
     *
     * @return the newly create SDP <tt>MediaDescription</tt>.
     */
    public static ContentPacketExtension createDescription(
                            CreatorEnum                  creator,
                            String                       contentName,
                            SendersEnum                  senders,
                            List<MediaFormat>            formats,
                            List<RTPExtension>           rtpExtensions,
                            DynamicPayloadTypeRegistry   dynamicPayloadTypes,
                            DynamicRTPExtensionsRegistry rtpExtensionsRegistry)
    {
        ContentPacketExtension content = new ContentPacketExtension();
        RtpDescriptionPacketExtension description
                                    = new RtpDescriptionPacketExtension();

        content.setCreator(creator);
        content.setName(contentName);

        //senders - only if we have them and if they are different from default
        if(senders != null && senders != SendersEnum.both)
            content.setSenders(senders);

        //RTP description
        content.addChildExtension(description);
        description.setMedia(formats.get(0).getMediaType().toString());

        //now fill in the RTP description
        for(MediaFormat fmt : formats)
        {
            description.addPayloadType(
                            formatToPayloadType(fmt, dynamicPayloadTypes));
        }

        // extmap attributes
        if (rtpExtensions != null && rtpExtensions.size() > 0)
        {
            for (RTPExtension extension : rtpExtensions)
            {
                byte extID
                    = rtpExtensionsRegistry.obtainExtensionMapping(extension);
                URI uri = extension.getURI();
                MediaDirection extDirection = extension.getDirection();
                String attributes = extension.getExtensionAttributes();
                SendersEnum sendersEnum = getSenders(extDirection,
                        false);
                RTPHdrExtPacketExtension ext = new RTPHdrExtPacketExtension();

                ext.setURI(uri);
                ext.setSenders(sendersEnum);
                ext.setID(Byte.toString(extID));
                ext.setAttributes(attributes);

                description.addChildExtension(ext);
            }
        }
        return content;
    }

    /**
     * Converts a specific {@link MediaFormat} into a new
     * {@link PayloadTypePacketExtension} instance.
     *
     * @param format the <tt>MediaFormat</tt> we'd like to convert.
     * @param ptRegistry the {@link DynamicPayloadTypeRegistry} to use for
     * formats that don't have a static pt number.
     *
     * @return the new <tt>PayloadTypePacketExtension</tt> which contains
     * <tt>format</tt>'s parameters.
     */
    public static PayloadTypePacketExtension formatToPayloadType(
            MediaFormat format,
            DynamicPayloadTypeRegistry ptRegistry)
    {
        PayloadTypePacketExtension ptExt = new PayloadTypePacketExtension();

        int payloadType = format.getRTPPayloadType();

        if (payloadType == MediaFormat.RTP_PAYLOAD_TYPE_UNKNOWN)
                payloadType = ptRegistry.obtainPayloadTypeNumber(format);

        ptExt.setId(payloadType);
        ptExt.setName(format.getEncoding());

        if(format instanceof AudioMediaFormat)
            ptExt.setChannels(((AudioMediaFormat)format).getChannels());

        ptExt.setClockrate((int)format.getClockRate());

        /*
         * Add the format parameters and the advanced attributes (as parameter
         * packet extensions).
         */
        for(Map.Entry<String, String> entry :
            format.getFormatParameters().entrySet())
        {
            ParameterPacketExtension ext = new ParameterPacketExtension();
            ext.setName(entry.getKey());
            ext.setValue(entry.getValue());
            ptExt.addParameter(ext);
        }
        for(Map.Entry<String, String> entry :
            format.getAdvancedAttributes().entrySet())
        {
            ParameterPacketExtension ext = new ParameterPacketExtension();
            ext.setName(entry.getKey());
            ext.setValue(entry.getValue());
            ptExt.addParameter(ext);
        }

        return ptExt;
    }

    /**
     * Returns the <tt>MediaType</tt> for <tt>content</tt> by looking for it
     * in the <tt>content</tt>'s <tt>description</tt>, if any.
     *
     * @param content the content to return the <tt>MediaType</tt> of
     * @return the <tt>MediaType</tt> for <tt>content</tt> by looking for it
     * in the <tt>content</tt>'s <tt>description</tt>, if any.
     * <tt>contentName</tt>
     */
    public static MediaType getMediaType(ContentPacketExtension content)
    {
        if (content == null)
            return null;

        // We will use content name for determining media type
        // if no RTP description is present(SCTP connection case)
        String mediaTypeName = content.getName();

        RtpDescriptionPacketExtension desc = getRtpDescription(content);
        if (desc != null)
        {
            String rtpMedia = desc.getMedia().toLowerCase();
            if (org.apache.commons.lang3.StringUtils.isNotEmpty(rtpMedia))
            {
                mediaTypeName = rtpMedia;
            }
        }
        if ("application".equals(mediaTypeName))
        {
            return MediaType.DATA;
        }
        return MediaType.parseString(mediaTypeName);
    }

}
