
package com.reactlibrary;

import static java.nio.charset.StandardCharsets.UTF_8;

import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.modules.core.DeviceEventManagerModule;

// androidx stuff
import androidx.annotation.Nullable;
import androidx.collection.SimpleArrayMap;

// nearby stuff
import com.google.android.gms.nearby.Nearby;
import com.google.android.gms.nearby.connection.AdvertisingOptions;
import com.google.android.gms.nearby.connection.ConnectionInfo;
import com.google.android.gms.nearby.connection.ConnectionLifecycleCallback;
import com.google.android.gms.nearby.connection.ConnectionResolution;
import com.google.android.gms.nearby.connection.ConnectionsClient;
import com.google.android.gms.nearby.connection.DiscoveredEndpointInfo;
import com.google.android.gms.nearby.connection.DiscoveryOptions;
import com.google.android.gms.nearby.connection.EndpointDiscoveryCallback;
import com.google.android.gms.nearby.connection.Payload;
import com.google.android.gms.nearby.connection.PayloadCallback;
import com.google.android.gms.nearby.connection.PayloadTransferUpdate;
import com.google.android.gms.nearby.connection.PayloadTransferUpdate.Status;
import com.google.android.gms.nearby.connection.Strategy;
import com.google.android.gms.nearby.connection.ConnectionsStatusCodes;


public class RNNearbyModule extends ReactContextBaseJavaModule {

    private ReactApplicationContext reactApplicationContext = getReactApplicationContext();
    
    private ConnectionsClient connectionsClient = Nearby.getConnectionsClient(reactApplicationContext); 

    public RNNearbyModule(ReactApplicationContext reactApplicationContext) {
	super(reactApplicationContext);
	this.reactApplicationContext = reactApplicationContext;
    }

    @Override
    public String getName() {
	return "RNNearby";
    }

    private void sendEvent(ReactContext reactContext,
			   String eventName,
			   @Nullable WritableMap message) {
	reactContext
	    .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
	    .emit(eventName, message);
    }

    @ReactMethod
    public void startDiscovery(String service_id, String strategy) {
	connectionsClient
	    .startDiscovery(service_id, endPointDiscoveryCallback(), setTheStrategy(strategy));
    }

    private EndpointDiscoveryCallback endPointDiscoveryCallback() {
	return new EndpointDiscoveryCallback() {
	    @Override
	    public void onEndpointFound(String endPointID, DiscoveredEndpointInfo info){
		WritableMap params = Arguments.createMap();
		params.putString("endpoint-name", info.getEndpointName());
		params.putString("service-id", info.getServiceId());
		sendEvent(reactApplicationContext, "endpoint-found", params);
	    }

	    @Override
	    public void onEndpointLost(String endPointID) {
		WritableMap params = Arguments.createMap();
		sendEvent(reactApplicationContext, "endpoint-lost", params);
	    }
	};
    }

    private DiscoveryOptions setTheStrategy(String strategy){
	// the default is point-to-point
	Strategy strat = Strategy.P2P_POINT_TO_POINT;
	switch(strategy){
	case "cluster":
	    strat = Strategy.P2P_CLUSTER;
	    break;
	case "point-to-point":
	    strat = Strategy.P2P_POINT_TO_POINT;
	    break;
	case "star":
	    strat = Strategy.P2P_STAR;
	    break;
	}
	return new DiscoveryOptions.Builder().setStrategy(strat).build();
    }

    @ReactMethod
    public void requestConnection(String name, String endPointID){
	connectionsClient
	    .requestConnection(name, endPointID, connectionLifecycleCallback());
    }

    private ConnectionLifecycleCallback connectionLifecycleCallback(){
	return new ConnectionLifecycleCallback() {
	    @Override
	    public void onConnectionInitiated(String endPointID, ConnectionInfo connectionInfo){
		WritableMap params = Arguments.createMap();
		params.putString("endpoint-id", endPointID);
		params.putString("auth-token", connectionInfo.getAuthenticationToken());
		params.putString("endpoint-name", connectionInfo.getEndpointName());
		params.putBoolean("is-incoming-connection", connectionInfo.isIncomingConnection());
		sendEvent(reactApplicationContext, "connection-initiated", params);
	    }

	    @Override
	    public void onConnectionResult(String endPointID, ConnectionResolution connectionResolution){
		String isSuccess = String.valueOf(connectionResolution.getStatus().isSuccess());
		String isCancelled = String.valueOf(connectionResolution.getStatus().isCanceled());
		String isInterrupted = String.valueOf(connectionResolution.getStatus().isInterrupted());

		String statusCode = getStatusCode(connectionResolution.getStatus().getStatusCode());
		
		WritableMap params = Arguments.createMap();
		params.putString("endpoint-id", endPointID);
		params.putString("is-success", isSuccess);
		params.putString("is-cancelled", isCancelled);
		params.putString("is-interrupted", isInterrupted);
		params.putString("status-code", statusCode);
		sendEvent(reactApplicationContext, "connection-resolution", params);
	    }

	    @Override
	    public void onDisconnected(String endPointID){
		WritableMap params = Arguments.createMap();
		params.putString("endpoint-id", endPointID);
		sendEvent(reactApplicationContext, "disconnected", params);
	    }
	};
    }

    @ReactMethod
    public void acceptConnection(String endPointID){
	connectionsClient
	    .acceptConnection(endPointID, payloadCallback());
    }


    // send everything to JS land to be dealt with
    public PayloadCallback payloadCallback(){
	return new PayloadCallback(){
	    // SimpleArrayMap<Long, Payload> incomingPayloads = new SimpleArrayMap<>();
	    
	    @Override
	    public void onPayloadReceived(String endPointID, Payload payload){
		// incomingPayloads.put(payload.getId(), payload);
		int payloadType = payload.getType();
		
		
		switch(payloadType){
		case Payload.Type.BYTES:
		    bytesDealer(endPointID, payload);
		    break;
		case Payload.Type.FILE:
		    fileDealer(endPointID, payload);
		    break;
		case Payload.Type.STREAM:
		    streamDealer(endPointID, payload);
		    break;
		default: 
		    break;
		}
	    }

	    @Override
	    public void onPayloadTransferUpdate(String endPointID, PayloadTransferUpdate payloadTransferUpdate){
		long payloadID = payloadTransferUpdate.getPayloadId();
		int payloadStatus = payloadTransferUpdate.getStatus();
		long bytesTransferred = payloadTransferUpdate.getBytesTransferred();
		long totalBytes = payloadTransferUpdate.getTotalBytes();

		// String statusCode = getStatusCode(payloadTransferUpdate.getStatus().getStatusCode());

		WritableMap params = Arguments.createMap();
		params.putString("payload-id", String.valueOf(payloadID));
		params.putInt("payload-status", payloadStatus);
		params.putString("bytes-transferred", String.valueOf(bytesTransferred));
		params.putString("total-bytes", String.valueOf(totalBytes));

		sendEvent(reactApplicationContext, "payload-transfer-update", params );
	    }
	};
    }

    private void bytesDealer(String endPointID, Payload payload) {
	byte[] receivedBytes = payload.asBytes();
	String textRecieved = new String(receivedBytes);
	String payloadID = String.valueOf(payload.getId());

	WritableMap params = Arguments.createMap();
	params.putString("payload-id", payloadID);
	params.putString("text-recieved", textRecieved);
	sendEvent(reactApplicationContext, "payload-received-bytes", params);
    }

    private void fileDealer(String endPointID, Payload payload){
	// maybe later
    }

    private void streamDealer(String endPointID, Payload payload){
	// SimpleArrayMap<Long, Payload> incomingPayloads = new SimpleArrayMap<>();
	// incomingPayloads.put(payload.getId(), payload);
	// create a solid implementation for this later 
    }

    @ReactMethod
    public void sendBytesPayload(String endpointID, String data){
	byte[] dataBytes = data.getBytes();
	Payload dataPayload = Payload.fromBytes(dataBytes);

	connectionsClient
	    .sendPayload(endpointID, dataPayload);
    }

    @ReactMethod
    public void maximumDataBytes(Callback callback){
	int maxBytes = connectionsClient.MAX_BYTES_DATA_SIZE;
	callback.invoke(maxBytes);
    }

    @ReactMethod
    public void startAdvertising(String name, String serviceID, String strategy){
	connectionsClient
	    .startAdvertising(name, serviceID, connectionLifecycleCallback(), advertisingOptions(strategy));
    }


    private AdvertisingOptions advertisingOptions(String strategy){
	Strategy strat = Strategy.P2P_POINT_TO_POINT;

	switch(strategy){
	case "point-to-point":
	    strat = Strategy.P2P_POINT_TO_POINT;
	    break;
	case "cluster":
	    strat = Strategy.P2P_CLUSTER;
	    break;
	case "star":
	    strat = Strategy.P2P_STAR;
	    break;
	}
	return new AdvertisingOptions.Builder().setStrategy(strat).build();
    }

    @ReactMethod
    public void stopAdvertising(){
	connectionsClient
	    .stopAdvertising();
    }

    @ReactMethod
    public void stopDiscovery(){
	connectionsClient
	    .stopDiscovery();
    }

    @ReactMethod
    public void disconnectFromEndpoint(String endpointID){
	connectionsClient
	    .disconnectFromEndpoint(endpointID);
    }

    @ReactMethod
    public void rejectConnection(String endpointID){
	connectionsClient
	    .rejectConnection(endpointID);
    }

    @ReactMethod
    public void cancelPayload(String payloadID){
	long longPayloadID = Long.parseLong(payloadID);

	connectionsClient
	    .cancelPayload(longPayloadID);
    }

    @ReactMethod
    public void stopAllEndpoints(){
	connectionsClient
	    .stopAllEndpoints();
    }

    private String getStatusCode(int statusCode){
	String stringStatusCode = new String();

	switch(statusCode){
	case ConnectionsStatusCodes.API_CONNECTION_FAILED_ALREADY_IN_USE:
	    stringStatusCode = "API_CONNECTION_FAILED_ALREADY_IN_USE";
	    break;
	case ConnectionsStatusCodes.MISSING_PERMISSION_ACCESS_COARSE_LOCATION:
	    stringStatusCode = "MISSING_PERMISSION_ACCESS_COARSE_LOCATION";
	    break;
	case ConnectionsStatusCodes.MISSING_PERMISSION_ACCESS_WIFI_STATE:
	    stringStatusCode = "MISSING_PERMISSION_ACCESS_WIFI_STATE";
	    break;
	case ConnectionsStatusCodes.MISSING_PERMISSION_BLUETOOTH:
	    stringStatusCode = "MISSING_PERMISSION_BLUETOOTH";
	    break;
	case ConnectionsStatusCodes.MISSING_PERMISSION_BLUETOOTH_ADMIN:
	    stringStatusCode = "MISSING_PERMISSION_BLUETOOTH_ADMIN";
	    break;
	case ConnectionsStatusCodes.MISSING_PERMISSION_CHANGE_WIFI_STATE:
	    stringStatusCode = "MISSING_PERMISSION_CHANGE_WIFI_STATE";
	    break;
	case ConnectionsStatusCodes.MISSING_PERMISSION_RECORD_AUDIO:
	    stringStatusCode = "MISSING_PERMISSION_RECORD_AUDIO";
	    break;
	case ConnectionsStatusCodes.STATUS_ALREADY_ADVERTISING:
	    stringStatusCode = "STATUS_ALREADY_ADVERTISING";
	    break;
	case ConnectionsStatusCodes.STATUS_ALREADY_CONNECTED_TO_ENDPOINT:
	    stringStatusCode = "STATUS_ALREADY_CONNECTED_TO_ENDPOINT";
	    break;
	case ConnectionsStatusCodes.STATUS_ALREADY_DISCOVERING:
	    stringStatusCode = "STATUS_ALREADY_DISCOVERING";
	    break;
	case ConnectionsStatusCodes.STATUS_ALREADY_HAVE_ACTIVE_STRATEGY:
	    stringStatusCode = "STATUS_ALREADY_HAVE_ACTIVE_STRATEGY";
	    break;
	case ConnectionsStatusCodes.STATUS_BLUETOOTH_ERROR:
	    stringStatusCode = "STATUS_BLUETOOTH_ERROR";
	    break;
	case ConnectionsStatusCodes.STATUS_CONNECTION_REJECTED:
	    stringStatusCode = "STATUS_CONNECTION_REJECTED";
	    break;
	case ConnectionsStatusCodes.STATUS_ENDPOINT_IO_ERROR:
	    stringStatusCode = "STATUS_ENDPOINT_IO_ERROR";
	    break;
	case ConnectionsStatusCodes.STATUS_ENDPOINT_UNKNOWN:
	    stringStatusCode = "STATUS_ENDPOINT_UNKNOWN";
	    break;
	case ConnectionsStatusCodes.STATUS_ERROR:
	    stringStatusCode = "STATUS_ERROR";
	    break;
	case ConnectionsStatusCodes.STATUS_NOT_CONNECTED_TO_ENDPOINT:
	    stringStatusCode = "STATUS_NOT_CONNECTED_TO_ENDPOINT";
	    break;
	case ConnectionsStatusCodes.STATUS_OK:
	    stringStatusCode = "STATUS_OK";
	    break;
	case ConnectionsStatusCodes.STATUS_OUT_OF_ORDER_API_CALL:
	    stringStatusCode = "STATUS_OUT_OF_ORDER_API_CALL";
	    break;
	case ConnectionsStatusCodes.STATUS_PAYLOAD_IO_ERROR:
	    stringStatusCode = "STATUS_PAYLOAD_IO_ERROR";
	    break;
	}

	return stringStatusCode;
    }
}
