package com.nket.android.achilles_android;

import android.content.Context;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.WindowManager;
import android.widget.Toast;

import org.json.JSONException;
import org.json.JSONObject;
import org.webrtc.AudioSource;
import org.webrtc.AudioTrack;
import org.webrtc.Camera2Enumerator;
import org.webrtc.DataChannel;
import org.webrtc.EglBase;
import org.webrtc.IceCandidate;
import org.webrtc.MediaConstraints;
import org.webrtc.MediaStream;
import org.webrtc.PeerConnection;
import org.webrtc.PeerConnectionFactory;
import org.webrtc.RendererCommon;
import org.webrtc.RtpReceiver;
import org.webrtc.SdpObserver;
import org.webrtc.SessionDescription;
import org.webrtc.SurfaceViewRenderer;
import org.webrtc.VideoCapturer;
import org.webrtc.VideoRenderer;
import org.webrtc.VideoSource;
import org.webrtc.VideoTrack;

import java.net.URISyntaxException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

import io.socket.client.IO;
import io.socket.client.Socket;
import io.socket.emitter.Emitter;

public class MainActivity extends AppCompatActivity {

    private static final String TAG = MainActivity.class.getSimpleName();

    private static final String VIDEO_TRACK_ID = "ARDAMSv0";
    private static final String AUDIO_TRACK_ID = "ARDAMSa0";
    private static final String AUDIO_ECHO_CANCELLATION_CONSTRAINT = "googEchoCancellation";
    private static final String AUDIO_AUTO_GAIN_CONTROL_CONSTRAINT = "googAutoGainControl";
    private static final String AUDIO_HIGH_PASS_FILTER_CONSTRAINT = "googHighpassFilter";
    private static final String AUDIO_NOISE_SUPPRESSION_CONSTRAINT = "googNoiseSuppression";

    // Socket.IO events key words
    private static final String JOINED = "joined";
    private static final String MESSAGE = "message";
    private static final String MESSAGE_READY = "ready";
    private static final String MESSAGE_ANSWER = "answer";
    private static final String MESSAGE_BYE = "monitor_say_bye";
    // Socket.IO room server url
    private static final String serverUrl = "https://192.168.232.215:8080/";
    // STUN server url
    private static final String googleStunServer = "stun:stunserver.org";

    private static final String[] MANDATORY_PERMISSIONS = {
            "android.permission.MODIFY_AUDIO_SETTINGS",
            "android.permission.RECORD_AUDIO",
            "android.permission.INTERNET"
    };

    private SurfaceViewRenderer mLocalVideoView;

    private ExecutorService mExecutorService;

    private Socket mSocket;

    private MediaStream mStream;
    private VideoSource mVideoSource;
    private AudioSource mAudioSource;
    private VideoTrack mLocalVideoTrack;
    private AudioTrack mLocalAudioTrack;
    private MediaConstraints mAudioConstraints = new MediaConstraints();
    private MediaConstraints mSdpConstraints = new MediaConstraints();
    private MediaConstraints mPcConstraints = new MediaConstraints();

    private String curMonitorId;

    private List<PeerConnection.IceServer> mIceServers = new ArrayList<>();

    private TrustManager[] mTrustManagers = new TrustManager[] {
            new X509TrustManager() {
                @Override
                public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {

                }

                @Override
                public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {

                }

                @Override
                public X509Certificate[] getAcceptedIssuers() {
                    return new X509Certificate[0];
                }
            }
    };

    private EglBase rootEglBase;
    private ProxyRenderer localProxyRenderer = new ProxyRenderer();
    private int videoWidth;
    private int videoHeight;
    private int videoFps;
    private VideoCapturer mVideoCapturer;

    private PeerConnectionFactory mPeerConnectionFactory;
    private PeerConnectionFactory.Options mOptions;
    private PeerConnection curPeerConnection;
    private Map<String, PeerConnection> mPeerConnectionMap = new HashMap<>();
    private SDPObserver mSDPObserver = new SDPObserver();
    private PCObserver mPCObserver = new PCObserver();

    private class ProxyRenderer implements VideoRenderer.Callbacks {
        private VideoRenderer.Callbacks target;

        synchronized public void renderFrame(VideoRenderer.I420Frame frame) {
            if (target == null) {
                VideoRenderer.renderFrameDone(frame);
                return;
            }
            target.renderFrame(frame);
        }

        synchronized public void setTarget(VideoRenderer.Callbacks target) {
            this.target = target;
        }
    }

    private class PCObserver implements PeerConnection.Observer {
        @Override
        public void onIceCandidate(final IceCandidate iceCandidate) {
            mExecutorService.execute(new Runnable() {
                @Override
                public void run() {
                    JSONObject candidate = new JSONObject();
                    jsonPut(candidate, "monitor_id", curMonitorId);
                    jsonPut(candidate, "type", "candidate");
                    jsonPut(candidate, "label", iceCandidate.sdpMLineIndex);
                    jsonPut(candidate, "id", iceCandidate.sdpMid);
                    jsonPut(candidate, "candidate", iceCandidate.sdp);

                    // Sending IceCandidate to monitor.
                    Log.d(TAG, "Sending IceCandidate to monitor:" + candidate.toString());
                    sendMessage(candidate);
                }
            });
        }

        @Override
        public void onAddStream(final MediaStream stream) {

        }

        @Override
        public void onRemoveStream(final MediaStream mediaStream) {

        }

        @Override
        public void onSignalingChange(PeerConnection.SignalingState state) {

        }

        @Override
        public void onIceConnectionChange(PeerConnection.IceConnectionState state) {

        }

        @Override
        public void onIceConnectionReceivingChange(boolean b) {

        }

        @Override
        public void onIceGatheringChange(PeerConnection.IceGatheringState state) {

        }

        @Override
        public void onIceCandidatesRemoved(IceCandidate[] iceCandidates) {

        }

        @Override
        public void onDataChannel(DataChannel dataChannel) {

        }

        @Override
        public void onRenegotiationNeeded() {

        }

        @Override
        public void onAddTrack(RtpReceiver rtpReceiver, MediaStream[] mediaStreams) {

        }

        private void jsonPut(JSONObject jsonObject, String key, Object value) {
            try {
                jsonObject.put(key, value);
            } catch (JSONException e) {
                Log.e(TAG, "JSON exception");
                throw new RuntimeException(e);
            }
        }
    }

    private class SDPObserver implements SdpObserver {
        @Override
        public void onCreateSuccess(final SessionDescription sessionDescription) {
            mExecutorService.execute(new Runnable() {
                @Override
                public void run() {
                    curPeerConnection.setLocalDescription(mSDPObserver, sessionDescription);
                    Log.d(TAG, "Set local SessionDescription and send message.");

                    // Sending offer to monitor.
                    JSONObject offer = new JSONObject();
                    jsonPut(offer, "monitor_id", curMonitorId);
                    jsonPut(offer, "type", "offer");
                    jsonPut(offer, "sdp", sessionDescription.description.toString());

                    Log.d(TAG, "Sending offer SesssionDescription to monitor.");
                    sendMessage(offer);
                }
            });
        }

        @Override
        public void onSetSuccess() {
            Log.d(TAG, "Remote SessionDescription is set succesfully");
        }

        @Override
        public void onCreateFailure(String error) {
            Log.d(TAG, "Failed to create offer SessionDescription: " + error);
        }

        @Override
        public void onSetFailure(String error) {

        }

        private void jsonPut(JSONObject jsonObject, String key, Object value) {
            try {
                jsonObject.put(key, value);
            } catch (JSONException e) {
                Log.e(TAG, "JSON exception");
                throw new RuntimeException(e);
            }
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        init();
        // initSocket();
    }

    @Override
    protected void onDestroy() {
        sayBye();
        super.onDestroy();
    }

    @Override
    protected void onStart() {
        super.onStart();

        if (mVideoCapturer != null) {
            Log.d(TAG, "Start video.");
            mVideoCapturer.startCapture(videoWidth, videoHeight, videoFps);
        }
    }

    @Override
    protected void onStop() {
        super.onStop();

        if (mVideoCapturer != null) {
            Log.d(TAG, "Stop video.");
            try {
                mVideoCapturer.stopCapture();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    private void init() {
        mLocalVideoView = (SurfaceViewRenderer) findViewById(R.id.local_video_view);

        // Init ExecutorService
        mExecutorService = Executors.newSingleThreadExecutor();

        // Socket.IO initialization
        initSocket();

        // Create video renderer
        rootEglBase = EglBase.create();
        Log.d(TAG, "Created video renderer.");

        mLocalVideoView.init(rootEglBase.getEglBaseContext(), null);
        mLocalVideoView.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL);
        mLocalVideoView.setEnableHardwareScaler(true);
        // Set ProxyRenderer target to SurfaceViewRenderer
        localProxyRenderer.setTarget(mLocalVideoView);
        mLocalVideoView.setMirror(true);

        // Check permission
        /*for (String permission : MANDATORY_PERMISSIONS) {
            if (checkCallingOrSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
                Log.w(TAG, "Permission " + permission + " is not granted.");
                // finish();
                return;
            }
        }*/

        DisplayMetrics displayMetrics = getDisplayMetrics();
        videoWidth = displayMetrics.widthPixels;
        videoHeight = displayMetrics.heightPixels;
        videoFps = 30;

        initPeerConnectionFactory();

        // Set STUN Server
        mIceServers.add(new PeerConnection.IceServer(googleStunServer));

        // Set default SessionDescription MediaConstraints
        mSdpConstraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveAudio", "true"));
        mSdpConstraints.mandatory.add(new MediaConstraints.KeyValuePair("OfferToReceiveVideo", "true"));

        // Set default AudioConstraints
        mAudioConstraints.mandatory.add(new MediaConstraints.KeyValuePair(AUDIO_ECHO_CANCELLATION_CONSTRAINT, "false"));
        mAudioConstraints.mandatory.add(new MediaConstraints.KeyValuePair(AUDIO_AUTO_GAIN_CONTROL_CONSTRAINT, "false"));
        mAudioConstraints.mandatory.add(new MediaConstraints.KeyValuePair(AUDIO_HIGH_PASS_FILTER_CONSTRAINT, "false"));
        mAudioConstraints.mandatory.add(new MediaConstraints.KeyValuePair(AUDIO_NOISE_SUPPRESSION_CONSTRAINT, "false"));

        // Enalble DTLS for normal calls
        mPcConstraints.optional.add(new MediaConstraints.KeyValuePair("RtpDataChannels", "true"));
    }

    /**
     * Socket.IO initialization
     */
    private void initSocket() {
        try {
            SSLContext sslContext = SSLContext.getInstance("TLS");
            sslContext.init(null, mTrustManagers, null);
            IO.Options options = new IO.Options();
            options.sslContext = sslContext;
            options.hostnameVerifier = new HostnameVerifier() {
                @Override
                public boolean verify(String hostname, SSLSession session) {
                    return true;
                }
            };

            mSocket = IO.socket(serverUrl, options);
            // Socket.IO events binding with key words
            mSocket.on(Socket.EVENT_CONNECT, onConnect);
            mSocket.on(Socket.EVENT_CONNECT_ERROR, onConnectError);
            mSocket.on(Socket.EVENT_CONNECT_TIMEOUT, onConnectError);
            mSocket.on(Socket.EVENT_DISCONNECT, onDisConnect);
            mSocket.on(JOINED, onJoined);
            mSocket.on(MESSAGE, onMessage);
            mSocket.on(MESSAGE_BYE, onBye);
            mSocket.connect();
        } catch (KeyManagementException e) {
            e.printStackTrace();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (URISyntaxException e) {
            e.printStackTrace();
        }
    }

    /**
     * PeerConnection factory initialization
     */
    private void initPeerConnectionFactory() {
        PeerConnectionFactory.initializeAndroidGlobals(getApplicationContext(), true);
        mOptions = new PeerConnectionFactory.Options();
        mOptions.networkIgnoreMask = 0;
        mPeerConnectionFactory = new PeerConnectionFactory(mOptions);
        Log.d(TAG, "Created PeerConnectionFactory.");
        mPeerConnectionFactory.setVideoHwAccelerationOptions(
                rootEglBase.getEglBaseContext(),
                rootEglBase.getEglBaseContext()
        );
    }

    /**
     * Android is connecting to server.
     */
    private Emitter.Listener onConnect = new Emitter.Listener() {
        @Override
        public void call(Object... args) {
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    Toast.makeText(getApplicationContext(), "Connecting to server!", Toast.LENGTH_LONG).show();
                    Log.d(TAG, "Socket.io connected to server!");

                    // Saying hello to server.
                     mSocket.emit("webcam");

                    // Get local media stream.
                    if (getMediaStream()) {
                        mSocket.emit("webcam_got_local_media");
                    }
                }
            });
        }
    };

    private Emitter.Listener onConnectError = new Emitter.Listener() {
        @Override
        public void call(final Object... args) {
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    Exception e = (Exception) args[0];
                    Log.e(TAG, "Socket.IO connected failed:", e);
                    Toast.makeText(getApplicationContext(), "Socket.IO connected failed.", Toast.LENGTH_LONG).show();
                }
            });
        }
    };

    private Emitter.Listener onDisConnect = new Emitter.Listener() {
        @Override
        public void call(Object... args) {

        }
    };

    /**
     * Received 'joined' from server.
     */
    private Emitter.Listener onJoined = new Emitter.Listener() {
        @Override
        public void call(Object... args) {
            Log.d(TAG, "I am in the room now!");
        }
    };

    /**
     * Received message from monitor.
     */
    private Emitter.Listener onMessage = new Emitter.Listener() {
        @Override
        public void call(final Object... args) {
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    try {
                        JSONObject message = (JSONObject)args[0];
                        Log.d(TAG, "Received a message: " + message);
                        if (message.getString("type").equals(MESSAGE_READY)) {
                            curMonitorId = message.getString("monitor_id");
                            Log.d(TAG, "Monitor " + curMonitorId + " is ready!");

                            start();
                        } else if (message.getString("type").equals(MESSAGE_ANSWER)) {
                            Log.d(TAG, "Received answer SessionDescription.");

                            SessionDescription sdp = new SessionDescription(
                                    SessionDescription.Type.ANSWER,
                                    message.getString("sdp")
                            );

                            curPeerConnection.setRemoteDescription(mSDPObserver, sdp);
                            Log.d(TAG, "Set remote SessionDescription.");
                            Toast.makeText(getApplicationContext(),
                                    "Monitor " + message.get("monitor_id" + " is connecting!"), Toast.LENGTH_LONG).show();
                        }
                    } catch (JSONException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    };

    /**
     * Received 'bye' from monitor, then remove local media stream and close this PeerConnection.
     */
    private Emitter.Listener onBye = new Emitter.Listener() {
        @Override
        public void call(final Object... args) {
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    String id = (String)args[0];
                    Log.d(TAG, "Monitor " + id + " said bye.");
                    mPeerConnectionMap.get(id).removeStream(mStream);
                    mPeerConnectionMap.get(id).close();
                    mPeerConnectionMap.remove(id);
                    Toast.makeText(getApplicationContext(), "Monitor " + id + " said bye.", Toast.LENGTH_LONG).show();
                }
            });
        }
    };

    /**
     * Saying bye to server.
     */
    private void sayBye() {
        Log.d(TAG, "Saying bye to server.");
        mSocket.emit("webcam_say_bye");
        mSocket.disconnect();
        mSocket.close();
    }

    /**
     * Get device resolution (width * height)
     * @return
     */
    private DisplayMetrics getDisplayMetrics() {
        DisplayMetrics displayMetrics = new DisplayMetrics();
        WindowManager windowManager = (WindowManager) getApplication().getSystemService(Context.WINDOW_SERVICE);
        windowManager.getDefaultDisplay().getRealMetrics(displayMetrics);
        return displayMetrics;
    }

    private VideoCapturer createVideoCapturer() {
        VideoCapturer videoCapturer = null;
        Log.d(TAG, "Creating capturer using camera2 API.");

        // Creating camera capturer
        Camera2Enumerator enumerator = new Camera2Enumerator(this);
        final String[] deviceNames = enumerator.getDeviceNames();
        Log.d(TAG, "Looking for back facing cameras.");
        for (String deviceName : deviceNames) {
            if (enumerator.isBackFacing(deviceName)) {
                Log.d(TAG, "Creating back facing camera capturer.");
                videoCapturer = enumerator.createCapturer(deviceName, null);
                break;
            }
        }

        if (videoCapturer == null) {
            Log.e(TAG, "Failed to open camera.");
            return null;
        }
        return videoCapturer;
    }

    /**
     * Get local Media and add tracks to media stream.
     * @return
     */
    private boolean getMediaStream() {
        // Add video stream
        mStream = mPeerConnectionFactory.createLocalMediaStream("ARDAMS");
        mVideoCapturer = createVideoCapturer();
        if(mStream.addTrack(createVideoTrack(mVideoCapturer)) &&
                mStream.addTrack(createAudioTrack(mAudioConstraints))) {
            Log.d(TAG, "Got local MediaStream.");
            return true;
        }

        // Add audio stream
        mStream.addTrack(createAudioTrack(mAudioConstraints));
        Log.e(TAG, "Can't get local MediaStream.");
        return false;
    }

    private VideoTrack createVideoTrack(VideoCapturer videoCapturer) {
        mVideoSource = mPeerConnectionFactory.createVideoSource(videoCapturer);
        videoCapturer.startCapture(videoWidth, videoHeight, videoHeight);

        mLocalVideoTrack = mPeerConnectionFactory.createVideoTrack(VIDEO_TRACK_ID, mVideoSource);
        mLocalVideoTrack.setEnabled(true);
        mLocalVideoTrack.addRenderer(new VideoRenderer(localProxyRenderer));
        return mLocalVideoTrack;
    }

    private AudioTrack createAudioTrack(MediaConstraints audioConstraints) {
        mAudioSource = mPeerConnectionFactory.createAudioSource(audioConstraints);
        mLocalAudioTrack = mPeerConnectionFactory.createAudioTrack(AUDIO_TRACK_ID, mAudioSource);
        mLocalAudioTrack.setEnabled(true);
        return mLocalAudioTrack;
    }

    /**
     * Create local PeerConnection.
     * Add local media stream to created PeerConnection.
     * Manage current PeerConnection, add it to dictionary.
     * Make a call.
     */
    private void start() {
        Log.d(TAG, ">>>>>>>> Start");

        // Create PeerConnection
        Log.d(TAG, ">>>>>>>> Create Peer Connection");
        curPeerConnection = mPeerConnectionFactory.createPeerConnection(mIceServers, mPcConstraints, mPCObserver);

        if (curPeerConnection == null) {
            Log.e(TAG, "Webcam failed to create local RTCPeerConnection.");
            Toast.makeText(this, "PeerConnection created failed.", Toast.LENGTH_LONG).show();
            return;
        }
        Log.d(TAG, "Created local PeerConnection.");

        // Add local Stream to PeerConnection
        curPeerConnection.addStream(mStream);

        // Add this monitor to dictionary of PeerConnections.
        mPeerConnectionMap.put(curMonitorId, curPeerConnection);

        // Make a call to monitor.
        doCall();
    }

    /**
     * Make a call and send local SessionDescription to monitor.
     */
    private void doCall() {
        Log.d(TAG, "Sending offer SessionDescription to monitor.");
        curPeerConnection.createOffer(mSDPObserver, mSdpConstraints);
    }

    /**
     * Send message by Socket.IO
     * @param message
     */
    private void sendMessage(Object message) {
        Log.d(TAG, "Sending message: " + message);
        mSocket.emit("message_to_monitor", message);
    }
}