package com.wdkl.ncs.janus.client;

import android.util.Log;

import com.wdkl.ncs.janus.rtc.WebRTCEngine;
import com.wdkl.ncs.janus.util.EnumType;

import org.json.JSONException;
import org.json.JSONObject;
import org.webrtc.IceCandidate;
import org.webrtc.SessionDescription;

import java.math.BigInteger;
import java.util.Random;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 参考1：https://blog.csdn.net/Java_lilin/article/details/104007291
 * 参考2：https://github.com/benwtrent/janus-gateway-android
 * 参考3：https://zhuanlan.zhihu.com/p/149324861?utm_source=wechat_session
 */
public class JanusClient implements WebSocketChannel.WebSocketCallback {
    private static final String TAG = "JanusClient";
    private ConcurrentHashMap<BigInteger, PluginHandle> attachedPlugins = new ConcurrentHashMap<>();
    private ConcurrentHashMap<String, Transaction> transactions = new ConcurrentHashMap<>();
    private BigInteger sessionId = null;
    private BigInteger currentHandleId = null;
    private JanusCallback janusCallback;

    private volatile boolean isKeepAliveRunning;
    private Thread keepAliveThread;

    private String janusUrl;
    private WebSocketChannel webSocketChannel;

    private EnumType.CallState callState;
    private EnumType.CallEndReason endReason;
    private EnumType.RefuseType refuseType;

    private BigInteger roomId;
    private BigInteger userId;
    public static String remoteUsername = "";

    private TimerTask timerTask;
    private Timer timer;

    public static final int ERROR_CREATE_ROOM = 0x01;
    public static final int ERROR_ON_MESSAGE = 0x02;
    public static final int ERROR_ON_CONNECT = 0x03;

    public JanusClient(String janusUrl, BigInteger userId) {
        this.janusUrl = janusUrl;
        this.userId = userId;
        webSocketChannel = new WebSocketChannel();
        webSocketChannel.setWebSocketCallback(this);

    }

    public WebSocketChannel getWebSocketChannel() {
        return webSocketChannel;
    }

    public void setWebSocketChannel(WebSocketChannel webSocketChannel) {
        this.webSocketChannel = webSocketChannel;
    }

    public void setJanusCallback(JanusCallback janusCallback) {
        this.janusCallback = janusCallback;
    }

    public BigInteger getSessionId() {
        return sessionId;
    }

    public BigInteger getCurrentHandleId() {
        return currentHandleId;
    }

    public EnumType.CallState getCallState() {
        return callState;
    }

    public void setCallState(EnumType.CallState callState) {
        this.callState = callState;
    }

    public EnumType.CallEndReason getEndReason() {
        return endReason;
    }

    public void setEndReason(EnumType.CallEndReason endReason) {
        this.endReason = endReason;
    }

    public EnumType.RefuseType getRefuseType() {
        return refuseType;
    }

    public void setRefuseType(EnumType.RefuseType refuseType) {
        this.refuseType = refuseType;
    }

    public BigInteger getRoomId() {
        return roomId;
    }

    public void setRoomId(BigInteger roomId) {
        this.roomId = roomId;
    }

    public void connect() {
        webSocketChannel.connect(janusUrl);
    }

    public void disConnect() {
        stopKeepAliveTimer();
        Log.i(TAG, "close ws");

        if (webSocketChannel != null) {
            try {
                Log.i(TAG,"close rtc");
                webSocketChannel.close();
                WebRTCEngine.getInstance().release();
            }catch (Exception e){
               // e.printStackTrace();
                Log.e(TAG, e.getMessage());
            }

            webSocketChannel = null;
        }
    }

    /**
     * 创建session，并得到反馈
     * 发出
     * {
     * "janus" : "create",
     * "transaction" : "<random alphanumeric string>"
     * }
     * 成功后的反馈
     * {
     * "janus" : "success",
     * "transaction" : "<same as the request>",
     * "data" : {
     * "id" : <unique integer session ID>
     * }
     * }
     * 错误时的反馈
     * error: a JSON object containing two fields:
     * code: a numeric error code, as defined in apierror.h;
     * reason: a verbose string describing the cause of the failure.
     * <p>
     * {
     * "janus" : "error",
     * "transaction" : "<same as the request>",
     * "error" : {
     * "code" : 458
     * "reason" : "Could not find session 12345678"
     * }
     * }
     * <p>
     * Once you've created a session, a new endpoint you can use is created in the server. Specifically, the new endpoint is constructed by concatenating the server root and the session identifier you've been returned (e.g., /janus/12345678).
     * <p>
     * This endpoint can be used in two different ways:
     * <p>
     * using a parameter-less GET request to the endpoint, you'll issue a long-poll request to be notified about events and incoming messages from this session;
     * using a POST request to send JSON messages, you'll interact with the session itself.
     */
    private void createSession() {
        //先声明回调
        String tid = randomString(12);
        transactions.put(tid, new Transaction(tid) {
            @Override
            public void onSuccess(JSONObject msg) throws Exception {
                JSONObject data = msg.getJSONObject("data");
                sessionId = new BigInteger(data.getString("id"));
                startKeepAliveTimer();
                if (janusCallback != null) {
                    janusCallback.onCreateSession(sessionId);
                }
            }
        });
        //再发送命令
        try {
            JSONObject obj = new JSONObject();
            obj.put("janus", "create");
            obj.put("transaction", tid);
            sendMessage(obj.toString());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 释放session
     */
    public void destroySession() {
        String tid = randomString(12);
        transactions.put(tid, new Transaction(tid) {
            @Override
            public void onSuccess(JSONObject msg) throws Exception {
                stopKeepAliveTimer();
                if (janusCallback != null) {
                    janusCallback.onDestroySession(sessionId);
                }
            }
        });
        try {
            JSONObject obj = new JSONObject();
            obj.put("janus", "destroy");
            obj.put("transaction", tid);
            obj.put("session_id", sessionId);
            sendMessage(obj.toString());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * Interacting with the session。建立session后，与session进行交互，得到handle id
     *
     * @param pluginName
     */
    public void attachPlugin(String pluginName) {
        String tid = randomString(12);
        transactions.put(tid, new Transaction(tid) {
            @Override
            public void onSuccess(JSONObject msg) throws Exception {
                JSONObject data = msg.getJSONObject("data");
                BigInteger handleId = new BigInteger(data.getString("id"));
                currentHandleId = handleId;
                if (janusCallback != null) {
                    janusCallback.onAttached(handleId);
                }
                PluginHandle handle = new PluginHandle(handleId);
                attachedPlugins.put(handleId, handle);
            }
        });

        try {
            JSONObject obj = new JSONObject();
            obj.put("janus", "attach");
            obj.put("transaction", tid);
            obj.put("plugin", pluginName);
            obj.put("session_id", sessionId);
            sendMessage(obj.toString());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * attach 到发布者的 handler 上，准备接收视频流
     * 每个发布者都要 attach 一遍，然后协商 sdp, SFU
     *
     * @param feedId
     */
    public void subscribeAttach(BigInteger feedId) {
        String tid = randomString(12);
        transactions.put(tid, new Transaction(tid, feedId) {
            @Override
            public void onSuccess(JSONObject msg, BigInteger feedId) throws Exception {
                JSONObject data = msg.getJSONObject("data");
                //其它发布者的 handleId
                BigInteger handleId = new BigInteger(data.getString("id"));
                if (janusCallback != null) {
                    janusCallback.onSubscribeAttached(handleId, feedId);
                }
                PluginHandle handle = new PluginHandle(handleId);
                attachedPlugins.put(handleId, handle);
            }
        });

        try {
            JSONObject obj = new JSONObject();
            obj.put("janus", "attach");
            obj.put("transaction", tid);
            obj.put("plugin", "janus.plugin.videoroom");
            obj.put("session_id", sessionId);
            sendMessage(obj.toString());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 给 plugin 发送消息
     *
     * @param handleId
     * @param sdp      webrtc session description
     */
    public void createOffer(BigInteger handleId, SessionDescription sdp, Boolean videoOn) {
        JSONObject message = new JSONObject();
        try {
            JSONObject publish = new JSONObject();
            publish.putOpt("audio", true);
            publish.putOpt("video", videoOn);

            JSONObject jsep = new JSONObject();
            jsep.putOpt("type", sdp.type);
            jsep.putOpt("sdp", sdp.description);

            message.putOpt("janus", "message");
            message.putOpt("body", publish);
            message.putOpt("jsep", jsep);
            message.putOpt("transaction", randomString(12));
            message.putOpt("session_id", sessionId);
            message.putOpt("handle_id", handleId);
        } catch (JSONException e) {
            e.printStackTrace();
        }

        sendMessage(message.toString());
    }

    /**
     * 开始订阅
     *
     * @param subscriptionHandleId
     * @param sdp
     */
    public void subscriptionStart(BigInteger subscriptionHandleId, SessionDescription sdp) {
        JSONObject message = new JSONObject();
        try {
            JSONObject body = new JSONObject();
            body.putOpt("request", "start");
            body.putOpt("room", roomId);

            message.putOpt("janus", "message");
            message.putOpt("body", body);
            message.putOpt("transaction", randomString(12));
            message.putOpt("session_id", sessionId);
            message.putOpt("handle_id", subscriptionHandleId);

            if (sdp != null) {
                JSONObject jsep = new JSONObject();
                jsep.putOpt("type", sdp.type);
                jsep.putOpt("sdp", sdp.description);
                message.putOpt("jsep", jsep);
            }
        } catch (JSONException e) {
            e.printStackTrace();
        }

        sendMessage(message.toString());
    }

    public void publish(BigInteger handleId, SessionDescription sdp, Boolean videoOn) {
        JSONObject message = new JSONObject();
        try {
            JSONObject publish = new JSONObject();
            //你也可以用configure请求代替publish。两者的功能在发布上是等效的，但从语义的角度来看，publish是发布时要发送的正确消息。configure请求也可以用于更新发布者会话的某些属性，在这种情况下，就不能用publish请求了
            publish.putOpt("request", "publish");
            publish.putOpt("audio", true);
            publish.putOpt("video", videoOn);

            JSONObject jsep = new JSONObject();
            jsep.putOpt("type", sdp.type);
            jsep.putOpt("sdp", sdp.description);

            message.putOpt("janus", "message");
            message.putOpt("body", publish);
            message.putOpt("jsep", jsep);
            message.putOpt("transaction", randomString(12));
            message.putOpt("session_id", sessionId);
            message.putOpt("handle_id", handleId);
        } catch (JSONException e) {
            e.printStackTrace();
        }

        sendMessage(message.toString());
    }

    /**
     * 订阅
     *
     * @param feedId 要订阅的ID
     */
    public void subscribe(BigInteger subscriptionHandleId, BigInteger feedId) {
        JSONObject message = new JSONObject();
        JSONObject body = new JSONObject();
        try {
            body.putOpt("ptype", "subscriber");
            body.putOpt("request", "join");
            body.putOpt("room", roomId);
            body.putOpt("feed", feedId);

            message.put("body", body);
            message.putOpt("janus", "message");
            message.putOpt("transaction", randomString(12));
            message.putOpt("session_id", sessionId);
            message.putOpt("handle_id", subscriptionHandleId);
        } catch (JSONException e) {
            e.printStackTrace();
        }

        sendMessage(message.toString());
    }

    public void trickleCandidate(BigInteger handleId, IceCandidate iceCandidate) {
        JSONObject candidate = new JSONObject();
        JSONObject message = new JSONObject();
        try {
            candidate.putOpt("candidate", iceCandidate.sdp);
            candidate.putOpt("sdpMid", iceCandidate.sdpMid);
            candidate.putOpt("sdpMLineIndex", iceCandidate.sdpMLineIndex);

            message.putOpt("janus", "trickle");
            message.putOpt("candidate", candidate);
            message.putOpt("transaction", randomString(12));
            message.putOpt("session_id", sessionId);
            message.putOpt("handle_id", handleId);
        } catch (JSONException e) {
            e.printStackTrace();
        }
        sendMessage(message.toString());
    }

    public void trickleCandidateComplete(BigInteger handleId) {
        JSONObject candidate = new JSONObject();
        JSONObject message = new JSONObject();
        try {
            candidate.putOpt("completed", true);

            message.putOpt("janus", "trickle");
            message.putOpt("candidate", candidate);
            message.putOpt("transaction", randomString(12));
            message.putOpt("session_id", sessionId);
            message.putOpt("handle_id", handleId);
        } catch (JSONException e) {
            e.printStackTrace();
        }
        sendMessage(message.toString());
    }

    /**
     * 创建房间
     *
     * @param handleId
     * @param newRoomId {
     *                  "request" : "create",
     *                  "room" : <unique numeric ID, optional, chosen by plugin if missing>,
     *                  "permanent" : <true|false, whether the room should be saved in the config file, default=false>,
     *                  "description" : "<pretty name of the room, optional>",
     *                  "secret" : "<password required to edit/destroy the room, optional>",
     *                  "pin" : "<password required to join the room, optional>",
     *                  "is_private" : <true|false, whether the room should appear in a list request>,
     *                  "allowed" : [ array of string tokens users can use to join this room, optional],
     *                  ...
     *                  }
     *                  <p>
     *                  反馈
     *                  <p>
     *                  {
     *                  "janus": "success",
     *                  "session_id": 6421975129252707,
     *                  "transaction": "m2D5tESZ3fwb",
     *                  "sender": 537399609431468,
     *                  "plugindata": {
     *                  "plugin": "janus.plugin.videoroom",
     *                  "data": {
     *                  "videoroom": "created",
     *                  "room": 12345678,
     *                  "permanent": false
     *                  }
     *                  }
     *                  }
     *                  <p>
     *                  {
     *                  "janus": "success",
     *                  "session_id": 1359598870403518,
     *                  "transaction": "4mC6b30kX0sr",
     *                  "sender": 2692286927487266,
     *                  "plugindata": {
     *                  "plugin": "janus.plugin.videoroom",
     *                  "data": {
     *                  "videoroom": "event",
     *                  "error_code": 427,
     *                  "error": "Room 12345678 already exists"
     *                  }
     *                  }
     *                  }
     */
    public void createRoom(BigInteger handleId, BigInteger newRoomId) {
        roomId = newRoomId;

        String tid = randomString(12);
        transactions.put(tid, new Transaction(tid) {
            @Override
            public void onSuccess(JSONObject msg) throws Exception {
                JSONObject data = msg.getJSONObject("plugindata").getJSONObject("data");
                if (data.getString("videoroom").equals("event")) {
                    if (data.has("error")) {
                        if (data.getInt("error_code") == 427) {
                            //拨打
                            if (EnumType.CallState.Outgoing == callState) {
                                destroyRoom(handleId, new DestroyRoomCallback() {
                                    @Override
                                    public void onSuccess() {
                                        createRoom(handleId, newRoomId);
                                    }

                                    @Override
                                    public void onFailed() {
                                        janusCallback.onError(ERROR_CREATE_ROOM, "创建房间失败");
                                    }
                                });
                            }
                            //接收
                            else if (EnumType.CallState.Incoming == callState) {
                                janusCallback.onCreateRoom(handleId);
                            }
                        } else {
                            janusCallback.onError(ERROR_CREATE_ROOM, "创建房间失败");
                        }
                    }
                } else if (data.getString("videoroom").equals("created")) {
                    janusCallback.onCreateRoom(handleId);
                }
            }

            @Override
            public void onError() {
                janusCallback.onError(ERROR_CREATE_ROOM, "创建房间失败");
            }
        });

        JSONObject message = new JSONObject();
        JSONObject body = new JSONObject();
        try {
            body.putOpt("request", "create");
            body.putOpt("room", newRoomId);
            message.put("body", body);

            message.putOpt("janus", "message");
            message.putOpt("transaction", tid);
            message.putOpt("session_id", sessionId);
            message.putOpt("handle_id", handleId);
        } catch (JSONException e) {
            e.printStackTrace();
        }

        sendMessage(message.toString());
    }

    /**
     * @param handleId "videoroom" : "destroyed",
     */
    public void destroyRoom(BigInteger handleId, DestroyRoomCallback destroyRoomCallback) {
        String tid = randomString(12);
        transactions.put(tid, new Transaction(tid) {
            @Override
            public void onSuccess(JSONObject msg) throws Exception {
                JSONObject data = msg.getJSONObject("plugindata").getJSONObject("data");
                if (destroyRoomCallback != null) {
                    if (data.getString("videoroom").equals("destroyed")) {
                        destroyRoomCallback.onSuccess();
                    } else {
                        destroyRoomCallback.onFailed();
                    }
                }
            }

            @Override
            public void onError() {
                if (destroyRoomCallback != null) {
                    destroyRoomCallback.onFailed();
                }
            }
        });

        JSONObject message = new JSONObject();
        JSONObject body = new JSONObject();
        try {
            body.putOpt("request", "destroy");
            body.putOpt("room", roomId);
            message.put("body", body);

            message.putOpt("janus", "message");
            message.putOpt("transaction", tid);
            message.putOpt("session_id", sessionId);
            message.putOpt("handle_id", handleId);
        } catch (JSONException e) {
            e.printStackTrace();
        }

        sendMessage(message.toString());
    }

    //给插件，发送消息，得到ack和event两个消息
    public void joinRoom(BigInteger handleId, String displayName) {
        JSONObject message = new JSONObject();
        JSONObject body = new JSONObject();
        try {
            body.putOpt("display", displayName);
            body.putOpt("ptype", "publisher");
            body.putOpt("request", "join");
            body.putOpt("id", userId); //发布者id，我司系统 sip_id
            body.putOpt("room", roomId);
            message.put("body", body);

            message.putOpt("janus", "message");
            message.putOpt("transaction", randomString(12));
            message.putOpt("session_id", sessionId);
            message.putOpt("handle_id", handleId);
        } catch (JSONException e) {
            e.printStackTrace();
        }

        sendMessage(message.toString());
    }

    public void leaveRoom() {
        JSONObject message = new JSONObject();
        JSONObject body = new JSONObject();
        try {
            body.putOpt("request", "leave");
            message.put("body", body);

            message.putOpt("janus", "message");
            message.putOpt("transaction", randomString(12));
            message.putOpt("session_id", sessionId);
            message.putOpt("handle_id", currentHandleId);
        } catch (JSONException e) {
            e.printStackTrace();
        }

        sendMessage(message.toString());
    }


    private synchronized void sendMessage(String message){
        if(webSocketChannel!=null){
            webSocketChannel.sendMessage(message);
        }
    }


    @Override
    public void onOpen() {
        if (sessionId == null) {
            createSession();
        }
    }

    @Override
    public void onMessage(String message) {
        //Log.d(TAG, "收到消息》》》" + message);
        try {
            JSONObject obj = new JSONObject(message);
            JanusMessageType type = JanusMessageType.fromString(obj.getString("janus"));
            String transaction = null;
            BigInteger sender = null;
            if (obj.has("transaction")) {
                transaction = obj.getString("transaction");
            }
            //是插件 handleId
            if (obj.has("sender")) {
                sender = new BigInteger(obj.getString("sender"));
            }
            PluginHandle handle = null;
            if (sender != null) {
                handle = attachedPlugins.get(sender);
            }
            switch (type) {
                case keepalive:
                    break;
                case ack:
                    break;
                case success:
                    if (transaction != null) {
                        Transaction cb = transactions.get(transaction);
                        if (cb != null) {
                            try {
                                if (cb.getFeedId() != null) {
                                    cb.onSuccess(obj, cb.getFeedId());
                                } else {
                                    cb.onSuccess(obj);
                                }
                                transactions.remove(transaction);
                            } catch (Exception e) {
                                e.printStackTrace();
                            }
                        }
                    }
                    break;
                case error: {
                    if (transaction != null) {
                        Transaction cb = transactions.get(transaction);
                        if (cb != null) {
                            cb.onError();
                            transactions.remove(transaction);
                        }
                    }
                    break;
                }
                case hangup: {

                    if (handle != null) {

                        if (janusCallback != null) {

                            janusCallback.onHangup(handle.getHandleId());
                        }
                    }
                    break;
                }
                case detached: {
                    if (handle != null) {
                        if (janusCallback != null) {
                            janusCallback.onDetached(handle.getHandleId());
                        }
                    }
                    break;
                }
                case event: {   //不进行 transaction 处理
                    if (handle != null) {
                        JSONObject plugin_data = null;
                        if (obj.has("plugindata")) {
                            plugin_data = obj.getJSONObject("plugindata");
                        }
                        if (plugin_data != null) {
                            JSONObject data = null;
                            JSONObject jsep = null;
                            if (plugin_data.has("data")) {
                                data = plugin_data.getJSONObject("data");
                            }
                            if (obj.has("jsep")) {
                                jsep = obj.getJSONObject("jsep");
                            }
                            if (janusCallback != null) {
                                janusCallback.onMessage(sender, handle.getHandleId(), data, jsep);
                            }
                        }
                    }
                }
                case trickle:
                    if (handle != null) {
                        if (obj.has("candidate")) {
                            JSONObject candidate = obj.getJSONObject("candidate");
                            if (janusCallback != null) {
                                janusCallback.onIceCandidate(handle.getHandleId(), candidate);
                            }
                        }
                    }
                    break;
                case destroy:
                    if (janusCallback != null) {
                        janusCallback.onDestroySession(sessionId);
                    }
                    break;
                case slowlink:
                    break;
            }
        } catch (JSONException ex) {
            if (janusCallback != null) {
                janusCallback.onError(ERROR_ON_MESSAGE, ex.getMessage());
            }
        }
    }

    /**
     * The long-poll request has a 30 seconds timeout. If it has no event to report, a simple keep-alive message will be triggered:
     */
    private void startKeepAliveTimer() {
        isKeepAliveRunning = true;
        if(timer!=null){
            timer.purge();
        }
        if(timerTask!=null){
            timerTask.cancel();
        }
        timer = new Timer();

        timerTask=new TimerTask() {
            @Override
            public void run() {
                if (webSocketChannel != null && webSocketChannel.isConnected()) {

                    JSONObject obj = new JSONObject();
                    try {
                        obj.put("janus", "keepalive");
                        obj.put("session_id", sessionId);
                        obj.put("transaction", randomString(12));
                        sendMessage(obj.toString());
                    } catch (JSONException e) {
                        e.printStackTrace();
                    }

                } else {
                    Log.e(TAG, "keepAlive failed websocket is null or not connected");
                }
            }
        };
        timer.schedule(timerTask,25000,25000);

//        keepAliveThread = new Thread(new Runnable() {
//            @Override
//            public void run() {
//                while (isKeepAliveRunning && !Thread.interrupted()) {
//                    synchronized (this) {
//                        try {
//                            Thread.sleep(25 * 1000);
//                        } catch (InterruptedException ex) {
//                            ex.printStackTrace();
//                        }
//
//                    }
//                }
//                Log.d(TAG, "keepAlive thread stopped");
//            }
//        }, "KeepAlive");
//        keepAliveThread.start();
    }

    private void stopKeepAliveTimer() {
        isKeepAliveRunning = false;
//        if (keepAliveThread != null) {
//            keepAliveThread.interrupt();
//        }
        if(timer!=null){
            timer.purge();
        }
        if(timerTask!=null){
            timerTask.cancel();
        }
    }

    @Override
    public void onClosed() {
        stopKeepAliveTimer();
    }

    @Override
    public void onError(String err) {
        if (janusCallback != null) {
            janusCallback.onError(ERROR_ON_CONNECT, err);
        }
    }

    public String randomString(int length) {
        String str = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
        Random random = new Random();
        StringBuilder sb = new StringBuilder(length);
        for (int i = 0; i < length; i++) {
            sb.append(str.charAt(random.nextInt(str.length())));
        }
        return sb.toString();
    }

    public interface DestroyRoomCallback {
        void onSuccess();

        void onFailed();
    }

    public interface JanusCallback {
        void onCreateSession(BigInteger sessionId);

        void onCreateRoom(BigInteger handleId);

        void onAttached(BigInteger handleId);

        /**
         * 订阅回调
         *
         * @param subscribeHandleId 订阅HandlerId
         * @param feedId            订阅 feedId
         */
        void onSubscribeAttached(BigInteger subscribeHandleId, BigInteger feedId);

        void onDetached(BigInteger handleId);

        void onHangup(BigInteger handleId);

        void onMessage(BigInteger sender, BigInteger handleId, JSONObject msg, JSONObject jsep);

        void onIceCandidate(BigInteger handleId, JSONObject candidate);

        void onDestroySession(BigInteger sessionId);

        void onError(int errorCode, String error);
    }
}
