/**
 * $Revision $
 * $Date $
 *
 * 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 org.xmpp.jnodes;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.xmpp.jnodes.nio.DatagramListener;
import org.xmpp.jnodes.nio.SelDatagramChannel;
import org.xmpp.packet.*;

import java.io.IOException;
import java.net.BindException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.util.*;
import java.math.*;

import org.ifsoft.*;
import org.ifsoft.rtp.*;
import org.ifsoft.rayo.*;

import com.sun.voip.server.*;
import com.sun.voip.*;

import org.dom4j.*;

import org.jitsi.impl.neomedia.codec.audio.opus.Opus;

import com.rayo.core.*;
import com.rayo.core.verb.*;
import com.rayo.core.xml.providers.*;



public class RelayChannel {

    private final SelDatagramChannel channelA;
    private final SelDatagramChannel channelB;
    private final SocketAddress addressA;
    private final SocketAddress addressB;
    private SocketAddress lastReceivedA;
    private SocketAddress lastReceivedB;
    private final SelDatagramChannel channelA_;
    private final SelDatagramChannel channelB_;
    private SocketAddress lastReceivedA_;
    private SocketAddress lastReceivedB_;
    private long lastReceivedTimeA;
    private long lastReceivedTimeB;
    private final int portA;
    private final int portB;
    private final String ip;
    private String attachment;

    private Byte localCryptoKey[];
    private Byte localCryptoSalt[];
    private Byte remoteCryptoKey[];
    private Byte remoteCryptoSalt[];
	private Encryptor encryptor = null;
	private Encryptor encryptor2 = null;
	private OutgoingCallHandler callHandler = null;
	private MemberReceiver memberReceiver = null;
    private int kt = 0;
    private int kt2 = 0;
    private int kt3 = 0;
    private Integer kt4 = new Integer((int)0);
    private ByteBuffer wBuffer = ByteBuffer.allocate(64 * 1024 );
    private RTPPacket lastAudioPacket = null;
    private RTPPacket lastVideoPacket = null;
    private JID from;
    private RayoComponent component;
    private Handset handset = null;

    private Long lastPacketTicks = Long.valueOf(0L);
    private Long lastVideoTimestamp = Long.valueOf(0L);
    private Integer lastAudioTimestamp = new Integer((int)0);

    private long decoder = 0;
    private final int sampleRate = 48000;
    private final int frameSizeInMillis = 20;
    private final int outputFrameSize = 2;
    private final int channels = 2;
    private int frameSizeInSamplesPerChannel = (sampleRate * frameSizeInMillis) / 1000;
    private int frameSizeInBytes = outputFrameSize * channels * frameSizeInSamplesPerChannel;

    private static final Logger Log = LoggerFactory.getLogger(RelayChannel.class);

    public static RelayChannel createLocalRelayChannel(final String host, final int minPort, final int maxPort) throws IOException {
        int range = maxPort - minPort;
        IOException be = null;

        for (int t = 0; t < 50; t++) {
            try {
                int a = Math.round((int) (Math.random() * range)) + minPort;
                a = a % 2 == 0 ? a : a + 1;
                return new RelayChannel(host, a);
            } catch (BindException e) {
                be = e;
            } catch (IOException e) {
                be = e;
            }
        }
        throw be;
    }

    public RelayChannel(final String host, final int portA) throws IOException {

        final int portB = portA + 2;

        addressA = new InetSocketAddress(host, portA);
        addressB = new InetSocketAddress(host, portB);

        channelA = SelDatagramChannel.open(null, addressA);
        channelB = SelDatagramChannel.open(null, addressB);

        channelA.setDatagramListener(new DatagramListener() {
            public synchronized void datagramReceived(final SelDatagramChannel channel, final ByteBuffer buffer, final SocketAddress address) {
                lastReceivedA = address;
                lastReceivedTimeA = System.currentTimeMillis();

                if (lastReceivedB != null) {
                    try {
                        buffer.flip();

						if (callHandler != null)
						{
							ByteBuffer bb = buffer.asReadOnlyBuffer();
							byte[] b = new byte[bb.remaining()];
							bb.get(b, 0, b.length);

							if (decryptMedia(b) == false) channelB.send(buffer, lastReceivedB);

						} else {

							channelB.send(buffer, lastReceivedB);
						}

                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        });

        channelB.setDatagramListener(new DatagramListener() {
            public synchronized void datagramReceived(final SelDatagramChannel channel, final ByteBuffer buffer, final SocketAddress address) {
                lastReceivedB = address;
                lastReceivedTimeB = System.currentTimeMillis();
                if (lastReceivedA != null) {
                    try {
                        buffer.flip();
                        channelA.send(buffer, lastReceivedA);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        });

        this.portA = portA;
        this.portB = portB;

        // RTCP Support
        SocketAddress addressA_ = new InetSocketAddress(host, portA + 1);
        SocketAddress addressB_ = new InetSocketAddress(host, portB + 1);

        channelA_ = SelDatagramChannel.open(null, addressA_);
        channelB_ = SelDatagramChannel.open(null, addressB_);

        channelA_.setDatagramListener(new DatagramListener() {
            public void datagramReceived(final SelDatagramChannel channel, final ByteBuffer buffer, final SocketAddress address) {
                lastReceivedA_ = address;

                if (lastReceivedB_ != null) {
                    try {
                        buffer.flip();
                        //Log.info("RTCP A->B " + buffer.toString());
                        channelB_.send(buffer, lastReceivedB_);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        });

        channelB_.setDatagramListener(new DatagramListener() {
            public void datagramReceived(final SelDatagramChannel channel, final ByteBuffer buffer, final SocketAddress address) {
                lastReceivedB_ = address;
                if (lastReceivedA_ != null) {
                    try {
                        buffer.flip();
                        //Log.info("RTCP B->A " + buffer.toString());
                        channelA_.send(buffer, lastReceivedA_);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        });

        this.ip = host;
    }

    public SocketAddress getAddressB() {
        return addressB;
    }

    public SocketAddress getAddressA() {
        return addressA;
    }

    public int getPortA() {
        return portA;
    }

    public int getPortB() {
        return portB;
    }

    public String getIp() {
        return ip;
    }

    public long getLastReceivedTimeA() {
        return lastReceivedTimeA;
    }

    public long getLastReceivedTimeB() {
        return lastReceivedTimeB;
    }

    public OutgoingCallHandler getCallHandler() {
        return  callHandler;
    }

    public void setCallHandler(OutgoingCallHandler callHandler) {
        this.callHandler = callHandler;
    }

    public Handset getHandset() {
        return handset;
    }

    public String getAttachment() {
        return attachment;
    }

    public void setAttachment(String attachment) {
        this.attachment = attachment;
    }

    public JID getFrom() {
        return from;
    }

    public void setFrom(JID from, RayoComponent component) {
        this.from = from;
        this.component = component;
    }

    public void setCrypto(Handset handset)
    {
		this.handset = handset;

		Byte localCryptoByte[] = Convert.fromBase64String(handset.localCrypto);
		Byte remoteCryptoByte[] = Convert.fromBase64String(handset.remoteCrypto);

        if(ArrayExtensions.getLength(localCryptoByte).intValue() != 30 || ArrayExtensions.getLength(remoteCryptoByte).intValue() != 30)
            Log.error("Unexpected key/salt length.");
        else {
            localCryptoKey = BitAssistant.subArray(localCryptoByte, Integer.valueOf(0), Integer.valueOf(16));
			localCryptoSalt = BitAssistant.subArray(localCryptoByte, Integer.valueOf(16), Integer.valueOf(14));

            remoteCryptoKey = BitAssistant.subArray(remoteCryptoByte, Integer.valueOf(0), Integer.valueOf(16));
			remoteCryptoSalt = BitAssistant.subArray(remoteCryptoByte, Integer.valueOf(16), Integer.valueOf(14));

			Log.info("Crypto Suite " + handset.cryptoSuite + " " + handset.localCrypto + " "  + handset.remoteCrypto + " " + " " + handset.codec + " " + handset.stereo);

			try {
				encryptor = new Encryptor(SDPCryptoSuite.getEncryptionMode(handset.cryptoSuite), localCryptoKey, localCryptoSalt, remoteCryptoKey, remoteCryptoSalt);
				encryptor2 = new Encryptor(SDPCryptoSuite.getEncryptionMode(handset.cryptoSuite), remoteCryptoKey, remoteCryptoSalt, localCryptoKey, localCryptoSalt);

            	decoder = Opus.decoder_create(sampleRate, channels);

            	if (decoder == 0) Log.error( "Opus decoder creation error ");

				if (decoder == 0)
				{
					handset.codec = "PCMU";
					Log.warn( "Opus decoder creation failure, PCMU will be used in default");
				}

			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}

    public String getMediaPreference()
    {
		String mediaPreference = "PCMU/8000/1";

		if (handset.codec == null || "OPUS".equals(handset.codec))
			mediaPreference = "PCM/48000/2";

		return mediaPreference;
	}

    public void close() {
        try {
            channelA.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            channelB.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            channelA_.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            channelB_.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

        if (callHandler != null) callHandler.cancelRequest("Channel closing..");

        if (decoder != 0)
        {
            Opus.decoder_destroy(decoder);
            decoder = 0;
        }

        SayCompleteEvent complete = new SayCompleteEvent();
        complete.setReason(SayCompleteEvent.Reason.valueOf("SUCCESS"));

		Presence presence = new Presence();
		presence.setFrom(getAttachment() + "@rayo." + component.getDomain() + "/" + this.from.getNode());
		presence.setTo(this.from);
		presence.getElement().add(component.getHandsetProvider().toXML(complete));
		component.sendPacket(presence);

    }

    public SelDatagramChannel getChannelA() {
        return channelA;
    }

    public SelDatagramChannel getChannelB() {
        return channelB;
    }

    public SelDatagramChannel getChannelA_() {
        return channelA_;
    }

    public SelDatagramChannel getChannelB_() {
        return channelB_;
    }

    private boolean isStunPacket(Byte bytes[])
    {
        if(bytes == null || ArrayExtensions.getLength(bytes).intValue() < 20)
            return false;

        Byte buffer[] = BitAssistant.subArray(bytes, Integer.valueOf(0), Integer.valueOf(2));
        Byte method = new Byte((byte)(buffer[1].byteValue() & 0xf));
        Byte num2 = new Byte((byte)(buffer[0].byteValue() & 1));
        Byte num3 = new Byte((byte)(buffer[1].byteValue() & 0x10));

        Integer count = new Integer(BitAssistant.toShortNetwork(BitAssistant.subArray(bytes, Integer.valueOf(2), Integer.valueOf(2)), Integer.valueOf(0)).shortValue());

        if(20 + count.intValue() < ArrayExtensions.getLength(bytes).intValue())
            return false;

        Byte buffer3[] = BitAssistant.subArray(bytes, Integer.valueOf(4), Integer.valueOf(4));
        Byte magicCookie[] = new Byte[] {Byte.valueOf((byte)33), Byte.valueOf((byte)18), Byte.valueOf((byte)-92), Byte.valueOf((byte)66)};

        for(Integer i = Integer.valueOf(0); i.intValue() < ArrayExtensions.getLength(magicCookie).intValue();)
        {
            Byte _var0 = magicCookie[i.intValue()];

            if(_var0 != null ? !_var0.equals(buffer3[i.intValue()]) : _var0 != buffer3[i.intValue()])
                return false;

            i = Integer.valueOf(i.intValue() + 1);
        }

		return true;
    }

    private Short getLength(Byte bytes[])
    {
        if(ArrayExtensions.getLength(bytes).intValue() < 4)
            return Short.valueOf((short)-1);
        else
            return BitAssistant.toShortNetwork(bytes, Integer.valueOf(2));
    }

	public void sendComfortNoisePayload()
	{

	}

    private Long getNextAudioTimestamp(Long clockRate)
    {
        Integer timestamp = lastAudioTimestamp;
        lastAudioTimestamp = Integer.valueOf(lastAudioTimestamp.intValue() + (new Integer((new Long((20L * clockRate.longValue()) / 1000L)).intValue())).intValue());
        return new Long((new Integer(timestamp.intValue())).longValue());
    }

	public synchronized void pushAudio(byte[] rtpData, byte[] opus)
	{
		try {

			if (lastAudioPacket != null)
			{
				RTPPacket newPacket = RTPPacket.parseBytes(BitAssistant.bytesToArray(rtpData));
				RTPPacket packet = RTPPacket.parseBytes(lastAudioPacket.getBytes());

				if (opus != null)
				{
					packet.setPayload(BitAssistant.bytesToArray(opus));
					packet.setTimestamp(getNextAudioTimestamp(Long.valueOf(48000)));

				} else { // ULAW
					packet.setPayload(newPacket.getPayload());
					packet.setTimestamp(getNextAudioTimestamp(Long.valueOf(8000)));
				}

				packet.setSequenceNumber(newPacket.getSequenceNumber());

				Byte pcms[] = encryptor2.encryptRTP(packet);

				wBuffer.clear();
				wBuffer.put( BitAssistant.bytesFromArray(pcms) );
				wBuffer.flip();

				if (getChannelB() != null && lastReceivedB != null)
				{
					getChannelB().send(wBuffer, lastReceivedB);

					kt++;

					if ( kt < 20 ) {
						Log.info( "+++ " + packet.getPayload().length );
					}
				}

			}

		} catch (Exception e) {

			Log.error( "RelayChannel pushAudio exception " + e );
			e.printStackTrace();
		}

	}

	public synchronized void pushVideo(RTPPacket videoPacket)
	{

	}

    private boolean decryptMedia(byte[] b)
    {
		Byte data[] = BitAssistant.bytesToArray(b);
		boolean decoded = false;

		if (isStunPacket(data) == false && encryptor != null)
		{
			RTPPacket packet = null;
			RTPPacket packet2 = null;
			RTCPPacket packets[] = null;
			try
			{
				packet2 = RTPPacket.parseBytes(BitAssistant.bytesToArray(b));

				if(packet2 != null)
				{
					decoded = true;
					//Log.info("Decoded media " + " " + packet2.getPayloadType() + " " + packet2.getSequenceNumber() + " " + packet2.getTimestamp());

					if (packet2.getPayloadType() == 0)		// PCMU (uLaw), mix audio
					{
						packet = encryptor.decryptRTP(data);

						if(packet != null)
						{
							lastAudioPacket = packet;

							byte[] byteBuffer = BitAssistant.bytesFromArray(packet.getPayload());
							int[] l16Buffer = new int[160];

							AudioConversion.ulawToLinear(byteBuffer, 0, byteBuffer.length, l16Buffer);

							memberReceiver = callHandler.getMemberReceiver();

							if (memberReceiver != null )
							{
								memberReceiver.handleWebRtcMedia(l16Buffer, packet.getSequenceNumber().shortValue());

								if ( kt2 < 10 ) {
									Log.info( "ULAW *** " + l16Buffer );
								}

								kt2++;
							}
						} else Log.warn("cannot decode packet " + packet2.getPayloadType() + " " + packet2.getSequenceNumber() + " " + packet2.getTimestamp());

					} else if (packet2.getPayloadType() == 111)	{	// OPUS, decode and mix audio

						packet = encryptor.decryptRTP(data);

						if(packet != null)
						{
							lastAudioPacket = packet;

							byte[] in = BitAssistant.bytesFromArray(packet.getPayload());
							int inputOffset = 0;
							int inputLength = in.length;

							int frameSizeInSamplesPerChannel = Opus.decoder_get_nb_samples(decoder, in, inputOffset, inputLength);

							if (frameSizeInSamplesPerChannel > 1)
							{
								int frameSizeInBytes = outputFrameSize * channels * frameSizeInSamplesPerChannel;

								byte[] output = new byte[frameSizeInBytes];
								frameSizeInSamplesPerChannel = Opus.decode(decoder, in, inputOffset, inputLength, output, 0, frameSizeInSamplesPerChannel, 0);

								memberReceiver = callHandler.getMemberReceiver();

								if (memberReceiver != null )
								{
									int[] l16Buffer = AudioConversion.bytesToLittleEndianInts(output);
									placeInStereo(l16Buffer);
									memberReceiver.handleWebRtcMedia(l16Buffer, packet.getSequenceNumber().shortValue());

									if ( kt2 < 10 ) {
										Log.info( "OPUS *** " + l16Buffer );
									}

									kt2++;
								}

							} else Log.info( "OPUS.decode fail..." +  frameSizeInSamplesPerChannel);

						} else Log.warn("cannot decode packet " + packet2.getPayloadType() + " " + packet2.getSequenceNumber() + " " + packet2.getTimestamp());


					} else if (packet2.getPayloadType() == 100)	{	// video (vp8) pass-thru
						decoded = false;

					} else {

						if (packet2.getPayloadType() != 13)
						{
							byte[] byteBuffer = BitAssistant.bytesFromArray(packet2.getPayload());

							//Log.info("Unexpected Payload " + packet2.getPayloadType() + " size " + byteBuffer.length);
							decoded = false;
						}
					}
				}
			}
			catch(Exception exception)
			{
				Log.error("RelayChannel - Could not decrypt data " + exception);
				exception.printStackTrace();
			}

		}
		//Log.info("Payload " + decoded + " " + b);
		return decoded;
	}

	private void placeInStereo(int[] buffer)
	{
		int stereo = 0;

		try {

			stereo = Integer.parseInt(handset.stereo);

		} catch(Exception exception) {

		}

		if (stereo > 0)
		{
			if (stereo > 90) stereo = 90;

			int pan = stereo - 90;
			pan = (pan < 0) ? -pan : pan;

			for (int j = 0; j < buffer.length; j+=2)
			{
				buffer[j] = (int) (buffer[j] * pan / 90);
			}

		} else {

			if (stereo < -90) stereo = -90;

			int pan = stereo + 90;
			pan = (pan < 0) ? -pan : pan;

			for (int j = 1; j < buffer.length; j+=2)
			{
				buffer[j] = (int) (buffer[j] * pan / 90);
			}
		}
	}
}
