package com.algobase.share.bluetooth;

import java.util.Locale;

import java.nio.ByteBuffer;

import android.app.Service;

import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothStatusCodes;

import android.content.Context;
import android.content.Intent;

import android.os.Build;
import android.os.Binder;
import android.os.IBinder;

import android.util.Log;

import java.util.List;
import java.util.UUID;

import com.algobase.share.bluetooth.*;
import com.algobase.share.system.*;


public class BluetoothLeService extends Service {

  public final static String ACTION_ERROR        = "ACTION_ERROR";
  public final static String ACTION_CONNECTED    = "ACTION_CONNECTED";
  public final static String ACTION_DISCONNECTED = "ACTION_DISCONNECTED";
  public final static String ACTION_SERVICES_DISCOVERED = 
                                                   "ACTION_SERVICES_DISCOVERED";
  public final static String ACTION_DATA_AVAILABLE  = "ACTION_DATA_AVAILABLE";


  public final static String EXTRA_TYPE  = "EXTRA_TYPE";
  public final static String EXTRA_ID    = "EXTRA_ID";
  public final static String EXTRA_NAME  = "EXTRA_NAME";
  public final static String EXTRA_ADDR  = "EXTRA_ADDR";
  public final static String EXTRA_DATA  = "EXTRA_DATA";
  public final static String EXTRA_BYTES = "EXTRA_BYTES";

  final static int PROPERTY_READ 
                           = BluetoothGattCharacteristic.PROPERTY_READ;
  final static int PROPERTY_WRITE 
                           = BluetoothGattCharacteristic.PROPERTY_WRITE;
  final static int PROPERTY_INDICATE 
                           = BluetoothGattCharacteristic.PROPERTY_INDICATE;
  final static int PROPERTY_NOTIFY 
                           = BluetoothGattCharacteristic.PROPERTY_NOTIFY;
  final static byte[] ENABLE_INDICATION_VALUE 
                           = BluetoothGattDescriptor.ENABLE_INDICATION_VALUE;
  final static byte[] ENABLE_NOTIFICATION_VALUE 
                           = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE;



/*
static String BATTERY_LEVEL            = "00002a19-0000-1000-8000-00805f9b34fb";
static String TEMPERATURE_MEASUREMENT  = "00002a1c-0000-1000-8000-00805f9b34fb";
static String HEART_RATE_MEASUREMENT   = "00002a37-0000-1000-8000-00805f9b34fb";
static String CYCLING_POWER_MEASUREMENT= "00002a63-0000-1000-8000-00805f9b34fb";
static String CSC_MEASUREMENT          = "00002a5b-0000-1000-8000-00805f9b34fb";
static String INDOOR_BIKE_DATA         = "00002ad2-0000-1000-8000-00805f9b34fb";
*/


  public final static String BATTERY_LEVEL =
                        AllGattCharacteristics.BATTERY_LEVEL;

  public final static String TEMPERATURE_MEASUREMENT =
                        AllGattCharacteristics.TEMPERATURE_MEASUREMENT;

  public final static String HEART_RATE_MEASUREMENT =
                        AllGattCharacteristics.HEART_RATE_MEASUREMENT;

  public final static String CYCLING_POWER_MEASUREMENT =
                        AllGattCharacteristics.CYCLING_POWER_MEASUREMENT;

  public final static String CYCLING_POWER_CONTROL_POINT =
                        AllGattCharacteristics.CYCLING_POWER_CONTROL_POINT;

  public final static String CSC_MEASUREMENT =
                        AllGattCharacteristics.CSC_MEASUREMENT;

  public final static String INDOOR_BIKE_DATA =
                        AllGattCharacteristics.INDOOR_BIKE_DATA;

  public final static String CLIENT_CHARACTERISTIC_CONFIGURATION = 
                        AllGattDescriptors.CLIENT_CHARACTERISTIC_CONFIGURATION;


  public class LocalBinder extends Binder {
     public BluetoothLeService getService() {
        return BluetoothLeService.this;
     }
  }



  public static BluetoothLeService getService(IBinder binder) {
    return (BluetoothLeService)((LocalBinder)binder).getService();
  }

  public abstract static class LogWriter {
     public void write(String txt) {}
  }


  // private

  IBinder iBinder;

  BluetoothManager  bluetoothManager;
  BluetoothAdapter  bluetoothAdapter;
  BluetoothGatt     bluetoothGatt;
  BluetoothDevice   bluetoothDevice = null;
  BluetoothGattCallback bluetoothGattCallback;

  String connectAddress = "";
  String deviceAddress = "";

  boolean autoConnect = false;

  String type = "any"; // "any", "hrt", "pwr", "cad", "tmp"

  boolean descriptorWritten = false;
  int  descriptorWriteCount = 0;

  boolean characteristicWritten = false;
  int  characteristicWriteCount = 0;


  int reconnect_count = 0;
  int connect_id = 0;

  LogWriter logWriter = null;

  void log(String txt) { 
    Log.d("BLE Service",txt);

    if (logWriter == null) return;

    if (txt.equals(""))
       logWriter.write("");
    else
      logWriter.write("BLE (" + type + ") " + txt);
  }

  public void setLogWriter(LogWriter lw) { logWriter = lw; }
  public void setAutoConnect(boolean b) { autoConnect = b; }
  public void setType(String t)  { type = t; }

  public String getType()  { return type; }


  String byte_string(byte[] bytes)
  { String bstr = "";
    if (bytes != null) { 
      for(byte b : bytes) bstr = String.format("%02X",b) + bstr;
    }
    return "0x"+ bstr;
  }



  void broadcastDeviceMessage(String action, BluetoothDevice device, 
                                             String data)
  { 
    Intent intent = new Intent(action);
    intent.putExtra(EXTRA_TYPE,type);
    intent.putExtra(EXTRA_ID,connect_id);

    if (device != null) {
      String addr = device.getAddress();
      String name = device.getName();
      if (name == null) name = addr;
      intent.putExtra(EXTRA_NAME,name);
      intent.putExtra(EXTRA_ADDR,addr);
    }

    if (data != null) intent.putExtra(EXTRA_DATA,data);

    sendBroadcast(intent);
  }


  int getIntValue(int type, byte[] bytes, int p)
  {
    switch(type) {

      case BluetoothGattCharacteristic.FORMAT_UINT8:
         return bytes[p] & 0xff;
   
      case BluetoothGattCharacteristic.FORMAT_UINT16: 
         return (bytes[p] & 0xff)  | ((bytes[p+1] & 0xff) << 8);
   
      case BluetoothGattCharacteristic.FORMAT_UINT32:
         return (bytes[p] & 0xff)  | ((bytes[p+1] & 0xff) <<  8)
                                   | ((bytes[p+2] & 0xff) << 16)
                                   | ((bytes[p+3] & 0xff) << 24);
   
      case BluetoothGattCharacteristic.FORMAT_SINT16:
         return (bytes[p] & 0xff)  | (bytes[p+1] << 8);
    }

   return 0;
 }


  void broadcast(String uuid, byte[] bytes) 
  {
    final int UINT8   = BluetoothGattCharacteristic.FORMAT_UINT8;
    final int UINT16  = BluetoothGattCharacteristic.FORMAT_UINT16;
 // final int UINT24  = BluetoothGattCharacteristic.FORMAT_UINT24;
    final int UINT32  = BluetoothGattCharacteristic.FORMAT_UINT32;
    final int SINT16  = BluetoothGattCharacteristic.FORMAT_SINT16;
    final int FLOAT32 = BluetoothGattCharacteristic.FORMAT_FLOAT;

    final Intent intent = new Intent(ACTION_DATA_AVAILABLE);
    intent.putExtra(EXTRA_TYPE,type);
    intent.putExtra(EXTRA_ID,connect_id);

    String bstr = byte_string(bytes);
/*
    log("");
    log("broadcast");
    log("uuid = " + uuid);
    log("bytes = " + bstr);
*/

    intent.putExtra(EXTRA_BYTES,bstr);

    if (uuid.startsWith("0000fff2")) {
      // temperature
      int x = getIntValue(SINT16,bytes,0);
      String s = "tmp " + x;
      log(s);
      intent.putExtra(EXTRA_DATA,s);
      sendBroadcast(intent);
      return;
    }

    if (uuid.equals(BATTERY_LEVEL))
    { int level = getIntValue(UINT8,bytes,0);
      String s = "bat " + level;
      log(s);
      intent.putExtra(EXTRA_DATA,s);
      sendBroadcast(intent);
      return;
    }

    // control point

    if (uuid.equals(CYCLING_POWER_CONTROL_POINT)) 
    { int x = getIntValue(UINT8,bytes,0);
      int y = getIntValue(UINT8,bytes,1);
      int z = getIntValue(UINT8,bytes,2);
      String s = String.format("ctrl %02X%02X%02X",x,y,z); 
      log(s);
      intent.putExtra(EXTRA_DATA,s);
      sendBroadcast(intent);
      return;
    }


    // notifications

    if (uuid.equals(INDOOR_BIKE_DATA)) {
      int p = 0;
      int flag = getIntValue(UINT16,bytes,p);
      p += 4;
/*
      flag & 0x0001: Instant Speed
      flag & 0x0002: Average Speed
      flag & 0x0004: Instant Cadence
      flag & 0x0008: Average Cadence
      flag & 0x0010: Total Distance
      flag & 0x0020: Resistance Level
      flag & 0x0040: Instant Power
      flag & 0x0080: Average Power
      flag & 0x0100: Expended Energy
      flag & 0x0200: Heart Ratge
      flag & 0x0400: Metabolic Equivalent
      flag & 0x0800: Elapsed Time
      flag & 0x1000: Remaining Time
*/
      int instant_speed = -1;
      int average_speed = -1;
      int instant_cadence = -1;
      int average_cadence = -1;
      int total_distance = -1;
      int resistance_level = -1;
      int instant_power = -1;
      int average_power = -1;
      int hour_energy = -1;
      int total_energy = -1;
      int minute_energy = -1;
      int expended_energy = -1;
      int heart_rate = -1;
      int metabolic_equivalent = -1;
      int elapsed_time = -1;
      int remaining_time = -1;

      if ((flag & 0x0001) != 0) { 
        instant_speed =  getIntValue(UINT16,bytes,p);
        p += 2;
      }
      if ((flag & 0x0002) != 0) { 
        average_speed =  getIntValue(UINT16,bytes,p);
        p += 2;
      }
      if ((flag & 0x0004) != 0) { 
        instant_cadence =  getIntValue(UINT16,bytes,p);
        p += 2;
      }
      if ((flag & 0x0008) != 0) { 
        average_cadence =  getIntValue(UINT16,bytes,p);
        p += 2;
      }
      if ((flag & 0x0010) != 0) { 
        int x = getIntValue(UINT8,bytes,p);
        int y = getIntValue(UINT8,bytes,p+1);
        int z = getIntValue(UINT8,bytes,p+2);
        total_distance = (z << 16) + (y << 8) + x;
        p += 3;
      }
      if ((flag & 0x0020) != 0) { 
        resistance_level = getIntValue(SINT16,bytes,p);
        p += 2;
      }
      if ((flag & 0x0040) != 0) { 
        instant_power = getIntValue(SINT16,bytes,p);
        p += 2;
      }
      if ((flag & 0x0080) != 0) { 
        average_power = getIntValue(SINT16,bytes,p);
        p += 2;
      }
      if ((flag & 0x0100) != 0) { 
        total_energy = getIntValue(UINT16,bytes,p);
        p += 2;
        hour_energy = getIntValue(UINT16,bytes,p);
        p += 2;
        minute_energy = getIntValue(UINT8,bytes,p);
        p += 1;
      }
      if ((flag & 0x0200) != 0) { 
        heart_rate = getIntValue(UINT8,bytes,p);
        p += 1;
      }
      if ((flag & 0x0400) != 0) { 
        metabolic_equivalent = getIntValue(UINT8,bytes,p);
        p += 1;
      }
      if ((flag & 0x0800) != 0) { 
        elapsed_time = getIntValue(UINT16,bytes,p);
        p += 2;
      }
      if ((flag & 0x1000) != 0) { 
        remaining_time = getIntValue(UINT16,bytes,p);
        p += 2;
      }

      String s = "fit" + " " + total_distance
                       + " " + instant_speed
                       + " " + instant_power
                       + " " + instant_cadence;

      intent.putExtra(EXTRA_DATA,s);
      sendBroadcast(intent);
      return;
    }

 
    if (uuid.equals(TEMPERATURE_MEASUREMENT)) {

      int p = 0;
      int flag = getIntValue(UINT8,bytes,p);
      p += 1;
/*
      celsius: (flag & 0x01) == 0
      time:    (flag & 0x02) != 0
      type:    (flag & 0x04) != 0
*/
      int x = getIntValue(UINT32,bytes,p);
      p += 4;

      float t = Float.intBitsToFloat(x);

      if ((flag & 0x01) != 0) {
        // Fahrenheit to Celsius
        t = 5*(t - 32)/9;
      }

      if ((flag & 0x02) != 0) {
       // time string
       int year = getIntValue(UINT16,bytes,p);
       p += 2;
       int month = getIntValue(UINT8,bytes,p);
       p += 1;
       int day = getIntValue(UINT8,bytes,p);
       p += 1;
       int hour = getIntValue(UINT8,bytes,p);
       p += 1;
       int min = getIntValue(UINT8,bytes,p);
       p += 1;
      }

      String s = String.format("tmp %d",(int)(0.5f + 100*t));

      log("broadcast: tmp");
      log("uuid = " + uuid);
      log("bytes = " + bstr);
      log(s);

      intent.putExtra(EXTRA_DATA,s);
      sendBroadcast(intent);
      return;
    }

    if (uuid.equals(HEART_RATE_MEASUREMENT)) 
    {
      //int flag = charact.getProperties();

      int p = 0;
      int flag = getIntValue(UINT8,bytes,p);
      p += 1;

      int hr = 0;
      int energy_expended = 0;

      if ((flag & 0x01) != 0) 
      { hr = getIntValue(UINT16,bytes,p);
        p += 2;
       }
      else
      { hr = getIntValue(UINT8,bytes,p);
        p += 1;
       }

      if ((flag & 0x08) != 0) {
        energy_expended = getIntValue(UINT16,bytes,p);
        p += 2;
      }
   
      String s = String.format("hrt %d",hr);

      int rr_count = 0;

      while (p < bytes.length-1)
      { int x = (int)(0.5f + getIntValue(UINT16,bytes,p)/1.024f); //msec
        p += 2;
        s += " " + x;
       }

      intent.putExtra(EXTRA_DATA,s);
      sendBroadcast(intent);
      return;
    }

    
    if (uuid.equals(CYCLING_POWER_MEASUREMENT)) 
    { 
      //int xflag = charact.getProperties(); // 16 bit
/*
      pedal_balance_present      = (flag & 0x0001) != 0);
      pedal_balance_reference    = (flag & 0x0002) != 0);
      accumulated_torque_present = (flag & 0x0004) != 0);
      accumulated_torque_source  = (flag & 0x0008) != 0);
      wheel_revolution_present   = (flag & 0x0010) != 0);
      crank_revolution_present   = (flag & 0x0020) != 0);
      extreme_force_present      = (flag & 0x0040) != 0);
      extreme_torque_present     = (flag & 0x0080) != 0);
      extreme_angles_present     = (flag & 0x0100) != 0);
      top_dead_spot_angle_present= (flag & 0x0200) != 0);
      bot_dead_spot_angle_present= (flag & 0x0400) != 0);
      accumulated_energy_present = (flag & 0x0800) != 0);
      offset_compensat_indicator = (flag & 0x1000) != 0);
*/

      int p = 0;

      int flag = getIntValue(UINT16,bytes,p); 
      p += 2;

      int power = getIntValue(SINT16,bytes,p); // mandatory
      p += 2;

      int pedal_balance = -1;
      int pedal_balance_ref = -1;
      int accu_torque = -1;
      int accu_torque_src = -1;
      int wheel_revolution_time = -1; // msec
      long wheel_revolution_sum = -1;
      int crank_revolution_time = -1; // msec
      int crank_revolution_sum = -1;

      if ((flag & 0x0001) != 0) {
         pedal_balance = getIntValue(UINT8,bytes,p);
         p += 1;
      }
      if ((flag & 0x0002) != 0) pedal_balance_ref = 1;
      if ((flag & 0x0004) != 0) {
         accu_torque = (int)(0.5f + getIntValue(UINT16,bytes,p)/32.0f);
         p += 2;
      }
      if ((flag & 0x0008) != 0) accu_torque_src = 1; 

      if ((flag & 0x0010) != 0) {
         wheel_revolution_sum = getIntValue(UINT32,bytes,p);
         p += 4;
         wheel_revolution_time = (int)(0.5f + getIntValue(UINT16,bytes,p)/2.048f);
         p += 2;
      }

      if ((flag & 0x0020) != 0) {
         crank_revolution_sum = getIntValue(UINT16,bytes,p);
         p += 2;
         crank_revolution_time = (int)(0.5f + getIntValue(UINT16,bytes,p)/1.024f);
         p += 2;
      }

      String s = "pwr" + " " + power 
                       + " " + pedal_balance
                       + " " + accu_torque
                       + " " + wheel_revolution_sum
                       + " " + wheel_revolution_time
                       + " " + crank_revolution_sum
                       + " " + crank_revolution_time;

      intent.putExtra(EXTRA_DATA,s);
      sendBroadcast(intent);
      return;
    }
    
    if (uuid.equals(CSC_MEASUREMENT)) 
    { 
      //int flag = charact.getProperties();

      int p = 0;
      int flag = getIntValue(UINT8,bytes,p);
      p += 1;

/*
      wheel_revolutions_present = ((flag & 0x01) != 0);
      crank_revolutions_present = ((flag & 0x02) != 0);
*/
  
      int cumulative_wheel_revolutions = 0;
      int last_wheel_event_time = 0; // msec

      int cumulative_crank_revolutions = 0;
      int last_crank_event_time = 0; // msec

      if ((flag & 0x01) != 0) {
         cumulative_wheel_revolutions = getIntValue(UINT32,bytes,p);
         p += 4;
         last_wheel_event_time = (int)(0.5f + getIntValue(UINT16,bytes,p)/1.024f);
         p += 2;
      }

      if ((flag & 0x02) != 0) {
         cumulative_crank_revolutions = getIntValue(UINT16,bytes,p);
         p += 2;
         last_crank_event_time = (int)(0.5f + getIntValue(UINT16,bytes,p)/1.024f);
         p += 2;
      }


      String s = "cad" + " " + cumulative_wheel_revolutions
                       + " " + last_wheel_event_time
                       + " " + cumulative_crank_revolutions
                       + " " + last_crank_event_time;

      intent.putExtra(EXTRA_DATA,s);
      sendBroadcast(intent);
      return;
    }


    log("");
    log("broadcast");
    log("uuid = " + uuid);
    log("bytes = " + bstr);

    sendBroadcast(intent);
  }


  @Override
  public IBinder onBind(Intent intent) {
     log("onBind");
     return iBinder;
  }

  @Override
  public boolean onUnbind(Intent intent) {
     log("onUnBind");
     close();
     return super.onUnbind(intent);
  }

  @Override
  public void onDestroy() {
   log("onDestroy");
   close();
   super.onDestroy();
 }


  void reconnect()
  { log("RECONNECT (" + reconnect_count + ")");
    log("addr = " + connectAddress);

    if (++reconnect_count >= 5) {
      log("CANCELED: too many trials");
      return;
    }

    bluetoothGatt.disconnect();
    bluetoothGatt.close();

    new MyThread() {
       public void run() {
          sleep(1000);
          bluetoothGatt = bluetoothDevice.connectGatt(getApplicationContext(), 
                                                 autoConnect,
                                                 bluetoothGattCallback,
                                                 BluetoothDevice.TRANSPORT_LE);
      }
    }.start();
  }
     


  @Override
  public void onCreate() 
  {
    super.onCreate();

    log("onCreate");

    iBinder = new LocalBinder();

    bluetoothManager = 
            (BluetoothManager)getSystemService(Context.BLUETOOTH_SERVICE);

    if (bluetoothManager == null) {
        log("bluetoothManager = null");
        return;
    }

    bluetoothAdapter = bluetoothManager.getAdapter();

    if (bluetoothAdapter == null) {
       log("bluetoothAdapter = null");
       return;
    }

    
    bluetoothGattCallback = new BluetoothGattCallback() {

        // callback methods for GATT events that the app cares about
        // for example, connection change and services discovered.

      /* 
         // callbacks
         onCharacteristicChanged
         onCharacteristicRead
         onCharacteristicWrite
         onConnectionStateChange
         onDescriptorRead
         onDescriptorWrite
         onMtuChanged
         onPhyRead
         onPhyUpdate
         onReadRemoteRssi
         onReliableWriteCompleted
         onServiceChanged
         onServiceDiscovered
       */


      @Override
      public void onConnectionStateChange(BluetoothGatt gatt, int status, 
                                                              int newState)
      { BluetoothDevice dev = gatt.getDevice();

        log("onConnectionStateChange");
        log("status = " + status);
        log("newState = " + newState);

        //BluetoothProfile.STATE_CONNECTING
        //BluetoothProfile.STATE_DISCONNECTING

        if (status != BluetoothGatt.GATT_SUCCESS)
        { String msg = "Connect Failed (" + status + ")";
          log(msg);
          broadcastDeviceMessage(ACTION_ERROR,dev,msg);
/*
           8: connection timeout
          19: connection terminated by peer
          22: connection terminated by local host 
         133: gatt error (device not found ?)
*/
          if (status == 22) reconnect();

          return;
         }
     
        if (newState == BluetoothProfile.STATE_CONNECTED)
        { log("STATE_CONNECTED");

          deviceAddress = dev.getAddress();

          String dat = "";
          if (reconnect_count > 0) dat += reconnect_count;
          broadcastDeviceMessage(ACTION_CONNECTED,dev,dat);

          reconnect_count = 0;

          // discover services after successful connection.

          if (!bluetoothGatt.discoverServices()) {
            log("discoverServices FAILED");
            gatt.close();
          }

          return;
         } 

        if (newState == BluetoothProfile.STATE_DISCONNECTED) 
        { log("STATE_DISCONNECTED");
          log("addr = " + connectAddress);

          if (!connectAddress.equals(""))
          { log("UNEXPECTED DISCONNECT");
            reconnect();
            return;
           }

          log("broadcast");

          deviceAddress = "";
          broadcastDeviceMessage(ACTION_DISCONNECTED,dev,null);
          gatt.close();
          return;
         }

      }

      @Override
      public void onDescriptorWrite(BluetoothGatt gatt, 
                                    BluetoothGattDescriptor descr, int status) 
      { log("onDescriptorWrite " + descriptorWriteCount);
        if (status == 0)
         descriptorWritten = true;
        else
         log("ERROR: status = " + status);
      }


      @Override
      public void onDescriptorRead(BluetoothGatt gatt, 
                                   BluetoothGattDescriptor descr, 
                                   int status, byte[] bytes) 
      {
        if (Build.VERSION.SDK_INT < 33) return;

        log("onDescriptorRead");
        log("value = " + byte_string(bytes));
        if (status != 0) log("ERROR: status = " + status);
      }


      @Override
      @SuppressWarnings("deprecation")
      public void onDescriptorRead(BluetoothGatt gatt,
                                   BluetoothGattDescriptor descr, 
                                   int status)
      {
        if (Build.VERSION.SDK_INT >= 33) return;

        log("onDescriptorRead");
        if (status != 0) log("ERROR: status = " + status);
      }


      
      @Override
      public void onServiceChanged(BluetoothGatt gatt) { 
        log("onServiceChanged");
      }

      @Override
      public void onServicesDiscovered(BluetoothGatt gatt, int status)
      { log("onServicesDiscovered");
        if (status == BluetoothGatt.GATT_SUCCESS) 
         broadcastDeviceMessage(ACTION_SERVICES_DISCOVERED,null,null);
        else
         log("ERROR: status = " + status);
      }


      @Override
      public void onCharacteristicRead(BluetoothGatt gatt,
                                       BluetoothGattCharacteristic charact,
                                       byte[] bytes,
                                       int status) 
      {
        if (Build.VERSION.SDK_INT < 33) return;

        String uuid = charact.getUuid().toString();

        log("onCharacteristicRead");
        log("uuid = " + uuid.substring(0,8));

        if (status == BluetoothGatt.GATT_SUCCESS)
        { log("value = " + byte_string(bytes));
          broadcast(uuid,bytes);
         }
        else
          log("ERROR: status = " + status);
       }


      @Override
      @SuppressWarnings("deprecation")
      public void onCharacteristicRead(BluetoothGatt gatt,
                                       BluetoothGattCharacteristic charact,
                                       int status)
      {
        if (Build.VERSION.SDK_INT >= 33) return;

        String uuid = charact.getUuid().toString();

        log("onCharacteristicRead");
        log("uuid = " + uuid.substring(0,8));

        if (status == BluetoothGatt.GATT_SUCCESS)
        { byte[] bytes = charact.getValue();
          log("value = " + byte_string(bytes));
          broadcast(uuid,bytes);
        }
        else
          log("ERROR: read status = " + status);
       }




      @Override
      public void onCharacteristicWrite(BluetoothGatt gatt,
                                        BluetoothGattCharacteristic charact,
                                        int status)
      { 
        String uuid = charact.getUuid().toString();

        log("onCharacteristicWrite " + characteristicWriteCount);
        log("uuid = " + uuid.substring(0,8));

        if (status == 0) 
        { //log("value = " + byte_string(charact.getValue()));
          log("success: status = " + status);
          characteristicWritten = true;
        }
        else
         log("ERROR: status = " + status);
      }


      @Override
      public void onCharacteristicChanged(BluetoothGatt gatt,
                                          BluetoothGattCharacteristic charact,
                                          byte[] bytes)
      {
        if (Build.VERSION.SDK_INT < 33) return;

        String uuid = charact.getUuid().toString();
        if (uuid.equals(CYCLING_POWER_CONTROL_POINT)) {
            log("onCharacteristicChanged " + uuid.substring(0,8));
        }
        broadcast(uuid,bytes);
      }


      @Override
      @SuppressWarnings("deprecation")
      public void onCharacteristicChanged(BluetoothGatt gatt,
                                          BluetoothGattCharacteristic charact)
      { 
        if (Build.VERSION.SDK_INT >= 33) return;

        String uuid = charact.getUuid().toString();
        byte[] bytes = charact.getValue();

        if (uuid.equals(CYCLING_POWER_CONTROL_POINT)) {
            log("onCharacteristicChanged " + uuid.substring(0,8));
        }

        broadcast(uuid,bytes);
      }

    };

  }


  public boolean connect(final String address, final int id)
  { log("");
    log("connect");
    log("addr = " + address);
    log("id   = " + id);

    connectAddress = address;
    reconnect_count = 0;
    connect_id = id;

    if (address == null) {
      log("address = null");
      return false;
     }

    if (bluetoothGatt != null && address.equals(deviceAddress))
    { // Previously connected device.  Try to reconnect.
      log("existing connection");
      return bluetoothGatt.connect();
    }

    bluetoothDevice = null;

    try {
     bluetoothDevice = bluetoothAdapter.getRemoteDevice(address);
    } catch(Exception ex) { log(ex.toString()); }
   
    if (bluetoothDevice == null) {
      log("Device not found.");
      return false;
    }

    log("new connection");

    bluetoothGatt = bluetoothDevice.connectGatt(this, 
                                                autoConnect,
                                                bluetoothGattCallback,
                                                BluetoothDevice.TRANSPORT_LE);
    if (bluetoothGatt == null) {
      log("ERROR: bluetoothGatt == null");
      return false;
    }

    return true;
  }


  public void disconnect() 
  { // Disconnects an existing connection or cancel a pending connection. 
    // The disconnection result is reported asynchronously through the
    // onConnectionStateChange(BluetoothGatt, int, int) callback.
     
    log("");
    log("disconnect");

    connectAddress = "";

    if (bluetoothGatt == null) {
      log("ERROR: bluetoothGatt == null");
      return;
    }

    bluetoothGatt.disconnect();
  }


  public void close() 
  { /* after using a given BLE device, this method must be called 
       to ensure resources are released properly.
     */
    log("close");

    if (bluetoothGatt == null) {
      log("Gatt = null");
      return;
    }

   bluetoothGatt.disconnect();
   bluetoothGatt.close();
   bluetoothGatt = null;

  }



  public void readCharacteristic(BluetoothGattCharacteristic charact) 
  { /* Request a read on a given BluetoothGattCharacteristic charact. 
       The read result is reported asynchronously through the 
       onCharacteristicRead(BluetoothGatt,BluetoothGattCharacteristic,int) 
       callback.
     */

    String uuid = charact.getUuid().toString();

    //if (!uuid.equals("0000fff2"))
    { log("");
      log("readCharacteristic");
      log("uuid = " + uuid.substring(0,8));
    }

    if (bluetoothGatt == null) {
      log("ERROR: bluetoothGatt = null");
      return;
    }

    int prop = charact.getProperties();
    if ((prop & PROPERTY_READ) == 0) {
      log("NO READ PROPERTY");
      return;
    }

    bluetoothGatt.readCharacteristic(charact);
  }



  @SuppressWarnings("deprecation")
  void writeDescriptor(final BluetoothGattCharacteristic charact, 
                       final byte[] bytes)
  {
    log("WRITE DESCRIPTOR");

    UUID uuid  = UUID.fromString(CLIENT_CHARACTERISTIC_CONFIGURATION);
    BluetoothGattDescriptor descriptor = charact.getDescriptor(uuid);

    if (descriptor == null) {
      log("ERROR: DESCRIPTOR == null");
      return;
    }

    if (Build.VERSION.SDK_INT < 33) {
      descriptor.setValue(bytes);
    }
 

    descriptorWritten = false;
    descriptorWriteCount = 0;

    new MyThread() {
      public void run() 
      { 
        for(int i=0; i<5; i++)
        { if (Build.VERSION.SDK_INT < 33) {
            if (bluetoothGatt.writeDescriptor(descriptor)) break;
          }
          else {
            if (bluetoothGatt.writeDescriptor(descriptor,bytes) == 
                                          BluetoothStatusCodes.SUCCESS) break;
          }
          log("writeDescriptor failed: " + i);
          sleep(250);
         }

       // wait until onDescriptorWrite gets called

       while (!descriptorWritten && descriptorWriteCount < 10) {
          descriptorWriteCount++;
          sleep(250);
       }

       if (!descriptorWritten) log("WRITE_DESCRIPTOR FAILED"); 
      }

    }.start();
 }


  @SuppressWarnings("deprecation")
  public void writeCharacteristic(final BluetoothGattCharacteristic charact,
                                  final byte[] bytes)
  {
    String uuid = charact.getUuid().toString();

    log("WRITE CHARACTERISTIC");
    log("uuid  = " + uuid.substring(0,8));
    log("value = " + byte_string(bytes));

    if (bluetoothGatt == null) {
      log("ERROR: bluetoothGatt = null");
      return;
    }

    if (Build.VERSION.SDK_INT < 33) {
      charact.setValue(bytes);
      charact.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT);
    //charact.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE);
    }

    characteristicWritten = false;
    characteristicWriteCount = 0;

    new MyThread() {
      public void run() 
      { for(int i =0; i<5; i++) 
        { if (Build.VERSION.SDK_INT < 33) {
            if (bluetoothGatt.writeCharacteristic(charact))break;
          }
          else
          { if (bluetoothGatt.writeCharacteristic(charact,bytes,
                           BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT) 
               == BluetoothStatusCodes.SUCCESS) break;
           }
          log("writeCharacteristic failed: i = " + i);
          sleep(250);
        }

        while (!characteristicWritten && characteristicWriteCount < 10) {
          characteristicWriteCount++;
          sleep(250);
        }

       if (!characteristicWritten) log("WRITE_CHARACTERISTIC FAILED"); 
     }
  }.start();

 }




  public void startNotification(final BluetoothGattCharacteristic charact)
  {
    // enables notification on a given characteristic

    String uuid = charact.getUuid().toString();

    log("");
    log("startNotification");
    log("uuid = " + uuid.substring(0,8));

    if (bluetoothGatt == null) {
       log("ERROR: bluetoothGatt = null");
       return;
    }

    final int prop = charact.getProperties();

    if ((prop & PROPERTY_NOTIFY) == 0 && (prop & PROPERTY_INDICATE) == 0) {
      log("NO NOTIFY/INDICATE PROPERTY");
      return;
    }

    if (!bluetoothGatt.setCharacteristicNotification(charact,true))
    { log("setCharacteristicNotification(true): FAILED");
      return;
     }

    // update descriptor of client characteristic configuration

    if ((prop & PROPERTY_NOTIFY) != 0)
    { log("ENABLE NOTIFICATION");
      writeDescriptor(charact,ENABLE_NOTIFICATION_VALUE);
     }
    else
      if ((prop & PROPERTY_INDICATE) != 0)
      { log("ENABLE INDICATION");
        writeDescriptor(charact,ENABLE_INDICATION_VALUE);
       }
  }

  public void stopNotification(BluetoothGattCharacteristic charact)
  { 
    String uuid = charact.getUuid().toString();

    log("");
    log("stopNotification " + uuid.substring(0,8));

    if (bluetoothGatt == null) {
      log("ERROR: bluetoothGatt = null");
      return;
    }

    if (!bluetoothGatt.setCharacteristicNotification(charact,false)) { 
      log("setCharacteristicNotification(false): FAILED");
    }

  }



  public List<BluetoothGattService> getSupportedGattServices() {

    /* Retrieves a list of supported GATT services on the connected device. 
       This should be invoked only after discoverServices() completes 
       successfully. Return the list of supported services.
     */

    if (bluetoothGatt == null) return null;

    return bluetoothGatt.getServices();
  }

}
