package com.reactnativebluetoothmessage;

import android.Manifest;
import android.annotation.SuppressLint;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothServerSocket;
import android.bluetooth.BluetoothSocket;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.Log;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.ActivityCompat;

import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.module.annotations.ReactModule;
import com.facebook.react.modules.core.DeviceEventManagerModule;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Objects;

@ReactModule(name = BluetoothMessageModule.NAME)
public class BluetoothMessageModule extends ReactContextBaseJavaModule {
  public static final String NAME = "BluetoothMessage";
  ReactApplicationContext reactContext;
  BluetoothAdapter bluetoothAdapter;
  boolean isConnected = false;
  ArrayList<BluetoothDevice> devices;
  SendReceive sendReceive;
  ClientClass client;
  ServerClass server;

  public BluetoothMessageModule(ReactApplicationContext context) {
    super(context);
    reactContext = context;
    bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
    IntentFilter scanIntentFilter = new IntentFilter(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED);
    BroadcastReceiver scanModeReceiver = new BroadcastReceiver() {
      @Override
      public void onReceive(Context context, @NonNull Intent intent) {
        String action = intent.getAction();
        if (action.equals(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED)) {
          int modeValue = intent.getIntExtra(BluetoothAdapter.EXTRA_SCAN_MODE, BluetoothAdapter.ERROR);
          if (modeValue == BluetoothAdapter.SCAN_MODE_CONNECTABLE) {
            WritableMap params = Arguments.createMap();
            params.putString("SCAN_MODE", "Device can receive connection");
            sendEvent(reactContext, "scanMode", params);
          } else if (modeValue == BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
            WritableMap params = Arguments.createMap();
            params.putString("SCAN_MODE", "Device is Discoverable and can receive connection");
            sendEvent(reactContext, "scanMode", params);
            Log.d(Constants.TAG,
              "Device is Discoverable and can receive connection");
          } else if (modeValue == BluetoothAdapter.SCAN_MODE_NONE) {
            WritableMap params = Arguments.createMap();
            params.putString("SCAN_MODE", "NOT Discoverable");
            sendEvent(reactContext, "scanMode", params);
            Log.d(Constants.TAG,
              "Device is NOT Discoverable and can't receive connection");
          } else {
            WritableMap params = Arguments.createMap();
            params.putString("SCAN_MODE", "ERROR");
            sendEvent(reactContext, "scanMode", params);
            Log.d(Constants.TAG, "ERROR");
          }
        }
      }
    };
    reactContext.registerReceiver(scanModeReceiver, scanIntentFilter);
  }

  @Override
  @NonNull
  public String getName() {
    return NAME;
  }

  private final BroadcastReceiver discoveryReceiver = new BroadcastReceiver() {
    @SuppressLint("MissingPermission")
    @Override
    public void onReceive(Context context, @NonNull Intent intent) {
      String action = intent.getAction();
      if (BluetoothDevice.ACTION_FOUND.equals(action)) {
        BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
        if (device.getName() != null) {
          devices.add(device);
          String deviceName = device.getName();
          String deviceHardwareAddress = device.getAddress();
          Log.d(Constants.TAG, "Found: " + deviceName + deviceHardwareAddress);
          sendEventHelper(deviceName, deviceHardwareAddress, "dvc");
        }
      }
    }
  };

  public void cancelDiscovery() {
    if (bluetoothAdapter.isDiscovering()) {
      Log.d(Constants.TAG, "Is Discovering");
      bluetoothAdapter.cancelDiscovery();
    } else {
      Log.d(Constants.TAG, "Not Discovering");
    }
  }

  Handler handler = new Handler(Looper.getMainLooper(), msg -> {
    switch (msg.what) {
      case Constants.STATE_LISTENING:
        isConnected = false;
        sendEventHelper("state", "LISTENING", "conn");
        break;
      case Constants.STATE_CONNECTING:
        isConnected = false;
        sendEventHelper("state", "CONNECTING", "conn");
        break;
      case Constants.STATE_CONNECTED:
        isConnected = true;
        sendEventHelper("state", "CONNECTED", "conn");
        break;
      case Constants.STATE_CONNECTION_FAILED:
        isConnected = false;
        sendEventHelper("state", "FAILED", "conn");
        break;
      case Constants.STATE_MESSAGE_RECEIVED:
        byte[] readBuff = (byte[]) msg.obj;
        String message = new String(readBuff, 0, msg.arg1);
        sendEventHelper("msg", message,"rcvr");
        break;
      case Constants.STATE_DISCONNECTED:
        isConnected = false;
        sendEventHelper("state", "DISCONNECTED", "conn");
        break;
    }
    return true;
  });

  private void sendEvent(ReactContext reactContext, String eventName, @Nullable WritableMap params) {
    reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit(eventName, params);
  }
  @ReactMethod public void addListener(String eventName) {}
  @ReactMethod public void removeListeners(Integer count) {}

  private void sendEventHelper(String msgName, String msg, String eventName) {
    WritableMap params = Arguments.createMap();
    params.putString(msgName, msg);
    sendEvent(reactContext, eventName, params);
  }

  @ReactMethod public void enable(Promise promise) {
    if (bluetoothAdapter.isEnabled()) {
      promise.resolve("Bluetooth [ALREADY ON]");
    } else {
      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
        String[] permissions = new String[] {
          Manifest.permission.BLUETOOTH_CONNECT,
          Manifest.permission.BLUETOOTH_ADVERTISE,
          Manifest.permission.BLUETOOTH_SCAN
        };
        if (ActivityCompat.checkSelfPermission(reactContext, Manifest.permission.BLUETOOTH_ADVERTISE) != PackageManager.PERMISSION_GRANTED) {
          ActivityCompat.requestPermissions(Objects.requireNonNull(getCurrentActivity()), permissions, 101);
          return;
        }
      } else {
        String[] permissions = new String[] {
          Manifest.permission.BLUETOOTH,
          Manifest.permission.BLUETOOTH_ADMIN
        };
        if (ActivityCompat.checkSelfPermission(reactContext, Manifest.permission.BLUETOOTH_ADMIN) != PackageManager.PERMISSION_GRANTED) {
          ActivityCompat.requestPermissions(Objects.requireNonNull(getCurrentActivity()), permissions, 101);
          return;
        }
      }
      bluetoothAdapter.enable();
      promise.resolve("Bluetooth [ENABLED]");
    }
  }

  @ReactMethod public void discover(Promise promise) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
      String[] permissions = new String[] {
        Manifest.permission.BLUETOOTH_CONNECT,
        Manifest.permission.BLUETOOTH_ADVERTISE,
        Manifest.permission.BLUETOOTH_SCAN
      };
      if (ActivityCompat.checkSelfPermission(reactContext, Manifest.permission.BLUETOOTH_ADVERTISE) != PackageManager.PERMISSION_GRANTED) {
        ActivityCompat.requestPermissions(Objects.requireNonNull(getCurrentActivity()), permissions, 101);
        return;
      }
    } else {
      String[] permissions = new String[] {
        Manifest.permission.BLUETOOTH,
        Manifest.permission.BLUETOOTH_ADMIN
      };
      if (ActivityCompat.checkSelfPermission(reactContext, Manifest.permission.BLUETOOTH_ADMIN) != PackageManager.PERMISSION_GRANTED) {
        ActivityCompat.requestPermissions(Objects.requireNonNull(getCurrentActivity()), permissions, 101);
        return;
      }
    }
//    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
//      String[] permissions = new String[] {
//        Manifest.permission.ACCESS_FINE_LOCATION,
//        Manifest.permission.ACCESS_COARSE_LOCATION
//      };
//      if (ActivityCompat.checkSelfPermission(reactContext, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
//        ActivityCompat.requestPermissions(Objects.requireNonNull(getCurrentActivity()), permissions, 101);
//        return;
//      }
//    }
    devices = new ArrayList<>();
    cancelDiscovery();
    if (!bluetoothAdapter.isEnabled()) {
      bluetoothAdapter.enable();
    } else {
      bluetoothAdapter.startDiscovery();
      if (bluetoothAdapter.isDiscovering()) {
        promise.resolve("Discovery [STARTED]");
      } else {
        promise.reject("DISCOVER","Discovery [FAILED]");
      }
    }

    IntentFilter intentActionFound = new IntentFilter(BluetoothDevice.ACTION_FOUND);
    reactContext.registerReceiver(discoveryReceiver, intentActionFound);
  }

  @ReactMethod public void discoverable(int duration, Promise promise) {
    Intent intentDiscoverable = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
    intentDiscoverable.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, duration);
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
      String[] permissions = new String[] {
        Manifest.permission.BLUETOOTH_CONNECT,
        Manifest.permission.BLUETOOTH_ADVERTISE,
        Manifest.permission.BLUETOOTH_SCAN
      };
      if (ActivityCompat.checkSelfPermission(reactContext, Manifest.permission.BLUETOOTH_ADVERTISE) != PackageManager.PERMISSION_GRANTED) {
          ActivityCompat.requestPermissions(Objects.requireNonNull(getCurrentActivity()), permissions, 101);
        return;
      }
    } else {
      String[] permissions = new String[] {
        Manifest.permission.BLUETOOTH,
        Manifest.permission.BLUETOOTH_ADMIN
      };
      if (ActivityCompat.checkSelfPermission(reactContext, Manifest.permission.BLUETOOTH_ADMIN) != PackageManager.PERMISSION_GRANTED) {
        ActivityCompat.requestPermissions(Objects.requireNonNull(getCurrentActivity()), permissions, 101);
        return;
      }
    }
    Objects.requireNonNull(getCurrentActivity()).startActivity(intentDiscoverable);
    server = new ServerClass();
    server.start();
    promise.resolve("Discoverable Invoked for "+duration+" seconds");
  }

  @ReactMethod public void connect(String deviceAddress) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
      String[] permissions = new String[] {
        Manifest.permission.BLUETOOTH_CONNECT,
        Manifest.permission.BLUETOOTH_ADVERTISE,
        Manifest.permission.BLUETOOTH_SCAN
      };
      if (ActivityCompat.checkSelfPermission(reactContext, Manifest.permission.BLUETOOTH_ADVERTISE) != PackageManager.PERMISSION_GRANTED) {
        ActivityCompat.requestPermissions(Objects.requireNonNull(getCurrentActivity()), permissions, 101);
        return;
      }
    } else {
      String[] permissions = new String[] {
        Manifest.permission.BLUETOOTH,
        Manifest.permission.BLUETOOTH_ADMIN
      };
      if (ActivityCompat.checkSelfPermission(reactContext, Manifest.permission.BLUETOOTH_ADMIN) != PackageManager.PERMISSION_GRANTED) {
        ActivityCompat.requestPermissions(Objects.requireNonNull(getCurrentActivity()), permissions, 101);
        return;
      }
    }
    if (devices.size() > 0) {
      for (BluetoothDevice device : devices) {
        if (Objects.equals(deviceAddress, device.getAddress())) {
          client = new ClientClass(device);
          client.start();
          Log.d(Constants.TAG,"Connecting " + device.getName());
          break;
        }
      }
    }
  }

  @ReactMethod public void send(String string) {
    if(sendReceive!=null){
      sendReceive.write(string.getBytes());
    }
  }

  @ReactMethod public void isConnected(Promise promise) {
    if(bluetoothAdapter.isEnabled() && isConnected){
      promise.resolve(true);
    } else {
      promise.resolve(false);
    }
  }

  private class ServerClass extends Thread {
    private final BluetoothServerSocket serverSocket;

    @SuppressLint("MissingPermission")
    public ServerClass(){
      BluetoothServerSocket tmp = null;
      try {
        tmp = bluetoothAdapter.listenUsingRfcommWithServiceRecord(Constants.APP_NAME,Constants.MY_UUID);
      } catch (IOException e) {
        e.printStackTrace();
      }
      serverSocket = tmp;
    }

    public void run() {
      BluetoothSocket socket;

      while (true){
        try{
          Message message = Message.obtain();
          message.what = Constants.STATE_LISTENING;
          handler.sendMessage(message);
          socket = serverSocket.accept();
        } catch (IOException e){
          e.printStackTrace();
          Message message = Message.obtain();
          message.what = Constants.STATE_CONNECTION_FAILED;
          handler.sendMessage(message);
          break;
        }

        if (socket!=null){
          Message message = Message.obtain();
          message.what = Constants.STATE_CONNECTED;
          handler.sendMessage(message);
          sendReceive = new SendReceive(socket);
          sendReceive.start();
          try {
            serverSocket.close();
          } catch (IOException e) {
            Log.e(Constants.TAG, "Could not close the connect socket", e);
          }
          break;
        }
      }
    }

    // Closes the connect socket and causes the thread to finish.
    public void cancel() {
      try {
        serverSocket.close();
      } catch (IOException e) {
        Log.e(Constants.TAG, "Could not close the connect socket", e);
      }
    }
  }

  @SuppressLint("MissingPermission")
  private class ClientClass extends Thread {
    private final BluetoothSocket socket;

    public ClientClass (@NonNull BluetoothDevice device) {
      BluetoothSocket tmp = null;

      try {
        tmp = device.createRfcommSocketToServiceRecord(Constants.MY_UUID);
      } catch (IOException e) {
        Log.d(Constants.TAG,"Client Class [NOT_INIT]");
        e.printStackTrace();
      }
      socket = tmp;
    }

    public void run() {
      cancelDiscovery();

      try {
        socket.connect();
        Message message = Message.obtain();
        message.what = Constants.STATE_CONNECTED;
        handler.sendMessage(message);

        sendReceive = new SendReceive(socket);
        sendReceive.start();
      } catch (IOException e) {
        try {
          socket.close();
        } catch (IOException closeException){
          Log.e(Constants.TAG, "closeException", closeException);
        }
        Message message = Message.obtain();
        message.what = Constants.STATE_CONNECTION_FAILED;
        handler.sendMessage(message);
      }
    }

    // Closes the client socket and causes the thread to finish.
    public void cancel() {
      try {
        socket.close();
      } catch (IOException e) {
        Log.e(Constants.TAG, "Could not close the client socket", e);
      }
    }
  }

  private class SendReceive extends Thread {
    private final BluetoothSocket bluetoothSocket;
    private final InputStream inStream;
    private final OutputStream outStream;

    public SendReceive(BluetoothSocket socket) {
      bluetoothSocket = socket;
      InputStream tmpIn = null;
      OutputStream tmpOut = null;

      try {
        tmpIn = bluetoothSocket.getInputStream();
        tmpOut = bluetoothSocket.getOutputStream();
      } catch (IOException e) {
        Log.d(Constants.TAG, "I/O [NOT_INIT]");
        e.printStackTrace();
      }

      inStream = tmpIn;
      outStream = tmpOut;
    }

    public void run() {
      byte[] buffer = new byte[1024];
      int bytes;

      while(true) {
        try {
          bytes = inStream.read(buffer);
          handler.obtainMessage(Constants.STATE_MESSAGE_RECEIVED, bytes,-1, buffer).sendToTarget();
          Log.d(Constants.TAG,String.valueOf(bytes));
        } catch (IOException e) {
          e.printStackTrace();
          Log.d(Constants.TAG,"[DISCONNECTED]");
          Message message = Message.obtain();
          message.what = Constants.STATE_CONNECTION_FAILED;
          handler.sendMessage(message);
          break;
        }
      }
    }

    public void write(byte[] bytes){
      try {
        outStream.write(bytes);
      } catch (IOException e) {
        e.printStackTrace();
        Log.d(Constants.TAG,"[DISCONNECTED]");
        Message message = Message.obtain();
        message.what = Constants.STATE_CONNECTION_FAILED;
        handler.sendMessage(message);
      }
    }

    // Call this method from the main activity to shut down the connection.
    public void cancel() {
      try {
        bluetoothSocket.close();
      } catch (IOException e) {
        Log.e(Constants.TAG, "Could not close the connect socket", e);
      }
    }
  }

  @ReactMethod public void disconnect() {
    cancelDiscovery();
    if (sendReceive != null) {
      sendReceive.cancel();
    }
    if(server != null) {
      server.cancel();
    }
    if(client != null){
      client.cancel();
    }
    Message message = Message.obtain();
    message.what = Constants.STATE_DISCONNECTED;
    handler.sendMessage(message);
  }

  @Override
  public void onCatalystInstanceDestroy() {
    super.onCatalystInstanceDestroy();
    cancelDiscovery();
    if (sendReceive != null) {
      sendReceive.cancel();
    }
    if (server != null) {
      server.cancel();
    }
    if (client != null) {
      client.cancel();
    }
    Message message = Message.obtain();
    message.what = Constants.STATE_DISCONNECTED;
    handler.sendMessage(message);
  }
}
