package com.algobase.share.bluetooth;

import java.util.List;
import java.util.ArrayList;

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

import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ComponentName;
import android.content.BroadcastReceiver;
import android.content.ServiceConnection;

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 com.algobase.share.system.*;
import com.algobase.share.bluetooth.*;


public class Bluetooth {

  final int UINT8 = BluetoothGattCharacteristic.FORMAT_UINT8;

  public static class HrateLeService extends BluetoothLeService {}
  public static class PowerLeService extends BluetoothLeService {}
  public static class CadenceLeService extends BluetoothLeService {}
  public static class TemperatureLeService extends BluetoothLeService {}
  public static class FitnessLeService extends BluetoothLeService {}


  public void writeLog(String txt) {}

  public void handle_message(String msg) {}
  public void update_device(String name, String addr, String status, 
                                                      String data) {}
  public void update_data(String[] data) {}


  Context context;

  BroadcastReceiver btReceiver = null;
  BluetoothLeService btLeService = null;
  ServiceConnection btServiceConnection;

  String bt_notify_uuid = "";
  String bt_notify_name = "";
  BluetoothGattCharacteristic bt_notify_characteristic = null;

  String bt_read_uuid = "";
  String bt_read_name = "";
  BluetoothGattCharacteristic bt_read_characteristic = null;

  String bt_battery_uuid = "";
  String bt_battery_name = "";
  BluetoothGattCharacteristic bt_battery_characteristic = null;

  String bt_control_uuid = "";
  String bt_control_name = "";
  BluetoothGattCharacteristic bt_control_characteristic = null;

  String bt_init_uuid = "";
  String bt_init_name = "";
  byte[] bt_init_bytes = null;
  BluetoothGattCharacteristic bt_init_characteristic = null;


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

  String  bt_device_addr = "";
  String  bt_device_name = "";

  String  bt_connect_addr = "";
  String  bt_connect_data = "";
  int     bt_connect_id = 0;
  int     bt_connect_count = 0;

  boolean bt_auto_connect = false;
  boolean bt_auto_control = true;

  boolean bt_log_data = false;


/*
  String  bt_charact_name = "";
  String  bt_charact_uuid = "";
  public BluetoothLeService getLeService() { return btLeService; }
  public void setCharacteristic(String name) { bt_charact_name = name; }
*/

  public void setNotifyCharacteristic(String uuid, String name) { 
      bt_notify_uuid = uuid;
      bt_notify_name = name;
  }

  public void setControlCharacteristic(String uuid, String name) { 
      bt_control_uuid = uuid;
      bt_control_name = name;
  }

  public void setBatteryCharacteristic(String uuid, String name) { 
      bt_battery_uuid = uuid;
      bt_battery_name = name;
  }


  public void setAutoConnect(boolean b) { bt_auto_connect = b; }

  public void setAutoControl(boolean b) { bt_auto_control = b; }

  public void setLogData(boolean b) { bt_log_data = b; }

  public boolean getAutoConnect()   { return bt_auto_connect; }
  public int    getConnectCount()   { return bt_connect_count; }
  public String getConnectAddress() { return bt_connect_addr; }
  public String getDeviceAddress()  { return bt_device_addr; }
  public String getDeviceName()     { return bt_device_name; }

/*
  public String getCharacteristic() { return bt_charact_name; }
*/

  public String getType() { return bt_type; }
  public String getLabel() { return bt_type.toUpperCase(); }


  String byteString(byte[] bytes)
  { String bstr = "0x";
    for(int i=0; i<bytes.length; i++) bstr += String.format("%02X",bytes[i]);
    return bstr;
  }

  public Bluetooth(Context ctxt, String type) 
  {
    context = ctxt;
    bt_type = type;

    bt_connect_id = (int)(System.currentTimeMillis() % 1000);

    bt_battery_name = "Battery Level";
    bt_battery_uuid = "00002a19";

    if (type.equals("hrt")) {
      bt_notify_name = "Heart Rate Measurement";
      bt_notify_uuid = "00002a37";
    }

    if (type.equals("pwr")) {
      bt_notify_name = "Cycling Power Measurement";
      bt_notify_uuid = "00002a63";
      bt_control_name = "Cycling Power Control Point";
      bt_control_uuid = "00002a66";
    }

    if (type.equals("cad")) {
      bt_notify_name = "CSC Measurement";
      bt_notify_uuid = "00002a5b";
    }

    if (type.equals("tmp")) {
      bt_notify_name = "Temperature Measurement";
    //bt_notify_uuid = "00002a1c";
      bt_notify_uuid = "0000fff6";

      bt_read_name = "Real Time Data";
      bt_read_uuid = "0000fff2";

/*
      bt_init_name = "Write Credentials";
      //bt_init_uuid = "0000fff1";
      bt_init_uuid = "0000fff3";
      bt_init_bytes = new byte[]{0x21,0x07,0x06,0x05,0x04,0x03,0x02,0x01,
                                 (byte)0xb8,0x22,0x00,0x00,0x00,0x00,0x00};
*/
    }

    if (type.equals("fit")) {
      bt_notify_name = "Indoor Bike Data";
      bt_notify_uuid = "00002ad2";
    }


    btReceiver = new BroadcastReceiver() {

    @Override
    public void onReceive(Context context, Intent intent) 
    {
      String action = intent.getAction();
      int id = intent.getIntExtra(BluetoothLeService.EXTRA_ID,0);

      String type = intent.getStringExtra(BluetoothLeService.EXTRA_TYPE);
      String name = intent.getStringExtra(BluetoothLeService.EXTRA_NAME);
      String addr = intent.getStringExtra(BluetoothLeService.EXTRA_ADDR);
      String data = intent.getStringExtra(BluetoothLeService.EXTRA_DATA);
      String bytes= intent.getStringExtra(BluetoothLeService.EXTRA_BYTES);

      if (!type.equals(bt_type)){
/*
        writeLog("");
        writeLog("action  =  " + action);
        writeLog("bt_type = " + bt_type);
        writeLog("type    = " + type);
        writeLog("id      = " + id);
        writeLog("bt_id   = " + bt_connect_id);
        writeLog("data    = " + data);
        writeLog("bytes   = " + bytes);
*/
        return;
      }

      if (id != bt_connect_id)
      { writeLog("WRONG ID: " + id + " != " + bt_connect_id);
        //writeLog("action =  " + action);
        return;
      }

      if (!action.equals(BluetoothLeService.ACTION_DATA_AVAILABLE) 
          || bt_log_data)
      { writeLog("");
        writeLog("BT Receiver (" + type + ")");
        writeLog(action);
        if (name != null) writeLog("name = " + name);
        if (addr != null) writeLog("addr = " + addr);
        if (data != null) writeLog("data = " + data);
       }

      if (action.equals(BluetoothLeService.ACTION_ERROR))
      { handle_message("Error: " + data);
        return;
      }

      if (action.equals(BluetoothLeService.ACTION_CONNECTED))
      { bt_device_addr = addr;
        bt_device_name = name;
        bt_connect_data = data; 
        update_device(name,addr,"connected",data);
     }


     if (action.equals(BluetoothLeService.ACTION_DISCONNECTED)) 
     { update_device(bt_device_name,bt_device_addr,"disconnected","");
       bt_device_addr = "";
       bt_device_name = "";
       return;
     }


     if (action.equals(BluetoothLeService.ACTION_SERVICES_DISCOVERED))
     { 
       // scan all available characteristics
       scanCharacteristics(btLeService);

       if (bt_notify_characteristic == null)
       { update_device(bt_device_name,bt_device_addr,"unavailable","");
         return;
        }

       update_device(bt_device_name,bt_device_addr,"available",bt_connect_data);

       bt_connect_count++;

       btLeService.startNotification(bt_notify_characteristic);

/*
       if (bt_control_characteristic != null) {
         btLeService.startNotification(bt_control_characteristic);
       }

       if (bt_init_characteristic != null) {
         btLeService.startNotification(bt_init_characteristic);
       }
*/


       new MyThread() {
         public void run() {

           sleep(1000);

           if (btLeService == null) // may happen - why ?
           { writeLog("ACTION_SERVICES_DISCOVERED: btLeService = null");
             return;
            }

           if (bt_battery_characteristic != null) { 
             btLeService.readCharacteristic(bt_battery_characteristic);
            }

           if (bt_read_characteristic != null) { 
             btLeService.readCharacteristic(bt_read_characteristic);
            }

           if (bt_auto_control && bt_control_characteristic != null)
           { // write auto control code on first connect
             writeControl("Zero Offset","0x0c");
            }


           if (bt_init_characteristic != null)
           { // write init bytes on first connect

             btLeService.startNotification(bt_init_characteristic);

             if (bt_init_bytes != null && bt_connect_count <= 1)
             { String bstr = byteString(bt_init_bytes);
               writeLog("Init: " + bt_init_name);
               writeLog(bstr);
               handle_message(bt_init_name + "  " + bstr);

               sleep(1000);
               btLeService.writeCharacteristic(bt_init_characteristic,
                                               bt_init_bytes);
              }
            }

          }

         }.start();

       return;
      } 


     if (action.equals(BluetoothLeService.ACTION_DATA_AVAILABLE)) 
     { if (data == null) return;
       String A[] = data.split(" ");
       if (A.length > 0) update_data(A);
       return;
     }

    }
  };

 }


 public void registerReceiver() 
 { IntentFilter intentFilter = new IntentFilter();
   intentFilter.addAction(BluetoothLeService.ACTION_ERROR);
   intentFilter.addAction(BluetoothLeService.ACTION_CONNECTED);
   intentFilter.addAction(BluetoothLeService.ACTION_DISCONNECTED);
   intentFilter.addAction(BluetoothLeService.ACTION_SERVICES_DISCOVERED);
   intentFilter.addAction(BluetoothLeService.ACTION_DATA_AVAILABLE);

   if (Build.VERSION.SDK_INT >= 26)
   context.registerReceiver(btReceiver,intentFilter,context.RECEIVER_EXPORTED);
   else
   context.registerReceiver(btReceiver,intentFilter);

 }

  public void unregisterReceiver() {
    context.unregisterReceiver(btReceiver);
  }


  void logCharacteristic(String name,BluetoothGattCharacteristic characteristic)
  {
    writeLog("");
    writeLog(name);
    if (characteristic == null) {
      writeLog("CHARACTERISTIC NOT FOUND");
      return;
    }

    int prop = characteristic.getProperties();
    String cuuid = characteristic.getUuid().toString();
    writeLog(cuuid.substring(0,8));
/*
    String cname = AllGattCharacteristics.lookup(cuuid);
    writeLog(cname);
*/

    if ((prop & BluetoothGattCharacteristic.PROPERTY_NOTIFY) != 0)
      writeLog("PROPERTY_NOTIFY");

    if ((prop & BluetoothGattCharacteristic.PROPERTY_INDICATE) != 0)
      writeLog("PROPERTY_INDICATE");
    
    if ((prop & BluetoothGattCharacteristic.PROPERTY_READ) != 0)
      writeLog("PROPERTY_READ");

    if ((prop & BluetoothGattCharacteristic.PROPERTY_WRITE) != 0)
      writeLog("PROPERTY_WRITE");

    if ((prop & BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) != 0)
      writeLog("PROPERTY_WRITE_NO_RESPONSE");

    if ((prop & BluetoothGattCharacteristic.PROPERTY_SIGNED_WRITE) != 0)
      writeLog("PROPERTY_SIGNED_WRITE");

    if ((prop & BluetoothGattCharacteristic.PROPERTY_EXTENDED_PROPS) != 0)
      writeLog("PROPERTY_EXTENDED_PROPS");

  }



  BluetoothGattCharacteristic scanCharacteristics(BluetoothLeService service, 
                                                  String c_uuid,
                                                  String c_name)
  {
    writeLog("");
    if (c_uuid == null)
       writeLog("Scan GATT Characteristics");
    else
    { writeLog("Find GATT Characteristic");
      writeLog(c_name);
      writeLog("uuid =  " + c_uuid);
     }

    List<BluetoothGattService> L = service.getSupportedGattServices();

    if (L == null || L.size() == 0) {
      writeLog("No GATT services found");
      return null;
    }

    // linear search for characteristic with uuid c_uuid in all 
    // available GATT Services.

    BluetoothGattCharacteristic characteristic = null;
    BluetoothGattCharacteristic battery_characteristic = null;

    for (BluetoothGattService s : L) 
    { String s_uuid = s.getUuid().toString();
      String service_name = AllGattServices.lookup(s_uuid);

      if (c_uuid == null) {
        writeLog("");
        writeLog(service_name);
      }

      List<BluetoothGattCharacteristic> c_list = s.getCharacteristics();

      for (BluetoothGattCharacteristic x : c_list) 
      { String uuid = x.getUuid().toString();
        String name = AllGattCharacteristics.lookup(uuid);

        if (c_uuid != null && uuid.startsWith(c_uuid)) characteristic = x;

        if (c_uuid == null) {
          int prop = x.getProperties();

          String props = "";

          if ((prop & BluetoothGattCharacteristic.PROPERTY_NOTIFY) != 0)
            props += "notify ";

          if ((prop & BluetoothGattCharacteristic.PROPERTY_INDICATE) != 0)
            props += "indicate ";

          if ((prop & BluetoothGattCharacteristic.PROPERTY_READ) != 0)
            props += "read ";

          if ((prop & BluetoothGattCharacteristic.PROPERTY_WRITE) != 0)
            props += "write ";

          if ((prop & BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) != 0)
            props += "write_no_response ";

          if ((prop & BluetoothGattCharacteristic.PROPERTY_SIGNED_WRITE) != 0)
            props += "signed_write ";

          if ((prop & BluetoothGattCharacteristic.PROPERTY_EXTENDED_PROPS) != 0)
            props += "extended_props ";

  
          writeLog("  " + name);
          writeLog("    uuid  = " + uuid.substring(0,8));
          writeLog("    props = " + props);
        }
      }
    }

    if (c_uuid == null) {
      logCharacteristic(c_name,characteristic);
    }

    return characteristic;
  }


  void scanCharacteristics(BluetoothLeService service)
  {
    writeLog("Scan GATT Characteristics");

    List<BluetoothGattService> L = service.getSupportedGattServices();

    if (L == null || L.size() == 0) {
      writeLog("No GATT services found");
      return;
    }

    // linear search for notify/battery/control characteristics  by uuid 

    bt_notify_characteristic = null;
    bt_control_characteristic = null;
    bt_battery_characteristic = null;

    for (BluetoothGattService s : L) 
    { String s_uuid = s.getUuid().toString();
      String service_name = AllGattServices.lookup(s_uuid);

      writeLog("");
      writeLog(service_name);

      List<BluetoothGattCharacteristic> c_list = s.getCharacteristics();

      for (BluetoothGattCharacteristic x : c_list) 
      { String uuid = x.getUuid().toString();
        String name = AllGattCharacteristics.lookup(uuid);

        if (!bt_notify_uuid.equals("") && uuid.startsWith(bt_notify_uuid))
          bt_notify_characteristic = x;

        if (!bt_read_uuid.equals("") && uuid.startsWith(bt_read_uuid)) 
          bt_read_characteristic = x;

        if (!bt_init_uuid.equals("") && uuid.startsWith(bt_init_uuid)) 
          bt_init_characteristic = x;

        if (!bt_control_uuid.equals("") && uuid.startsWith(bt_control_uuid)) 
          bt_control_characteristic = x;

        if (!bt_battery_uuid.equals("") && uuid.startsWith(bt_battery_uuid)) 
          bt_battery_characteristic = x;

        int prop = x.getProperties();

        String props = "";
        if ((prop & BluetoothGattCharacteristic.PROPERTY_NOTIFY) != 0)
          props += "notify ";
        if ((prop & BluetoothGattCharacteristic.PROPERTY_READ) != 0)
          props += "read ";
        if ((prop & BluetoothGattCharacteristic.PROPERTY_WRITE) != 0)
          props += "write ";
        if ((prop & BluetoothGattCharacteristic.PROPERTY_INDICATE) != 0)
          props += "indicate ";

        writeLog("  " + name);
        writeLog("    uuid  = " + uuid.substring(0,8));
        writeLog("    props = " + props);
      }
    }

    if (!bt_notify_name.equals(""))
      logCharacteristic(bt_notify_name,bt_notify_characteristic);

    if (!bt_read_name.equals(""))
      logCharacteristic(bt_read_name,bt_read_characteristic);

    if (!bt_init_name.equals(""))
      logCharacteristic(bt_init_name,bt_init_characteristic);

    if (!bt_control_name.equals(""))
      logCharacteristic(bt_control_name,bt_control_characteristic);

    if (!bt_battery_name.equals(""))
      logCharacteristic(bt_battery_name,bt_battery_characteristic);
  }


   public void status(final String what)
   { if (btLeService == null) return;
     if (what.equals("battery") && bt_battery_characteristic != null) {
       btLeService.readCharacteristic(bt_battery_characteristic);
     }
   }

   public void read()
   { if (btLeService == null) return;
     if (bt_read_characteristic != null) {
       btLeService.readCharacteristic(bt_read_characteristic);
     }
   }

   public void writeControl(String ctrl_name, String code_string) 
   { 
     if (bt_control_characteristic == null) {
       handle_message(ctrl_name + " failed.");
       return;
     }

     // code_string = "0x.." (pwr zero offst: 0x0c)

     int code = Integer.parseInt(code_string.substring(2),16); // hex
     int sz = (code_string.length() - 2)/2;

     byte[] bytes = new byte[sz];
     for(int i= 0; i< sz; i++) {
       bytes[i] = (byte)((code>>(8*i)) & 0xff);
     }

     btLeService.startNotification(bt_control_characteristic);
     String bstr = byteString(bytes);
     writeLog(ctrl_name + "  " + bt_control_name);
     writeLog(bstr);
     //handle_message(ctrl_name + "  " + bstr);
     handle_message(ctrl_name + " ...");
     btLeService.writeCharacteristic(bt_control_characteristic, bytes);
   }


   public void connect(final String address)
   { 
     bt_connect_id++;

     writeLog("");
     writeLog("connect " + bt_type);
     writeLog("connect id = " + bt_connect_id);
     writeLog("address = " + address);
     writeLog("");

     if (!bt_device_name.equals("")) disconnect();

     bt_device_addr = "";
     bt_connect_addr = address;
     bt_connect_count = 0;

     update_data(null);

     btServiceConnection = new ServiceConnection() 
     {
       @Override
       public void onServiceConnected(ComponentName cName, IBinder binder)
       { writeLog("onServiceConnected: " + bt_type);

         btLeService = BluetoothLeService.getService(binder);

         if (btLeService == null)
         { writeLog("btLeService = null");
           return;
          }

         btLeService.setType(bt_type);
         btLeService.setAutoConnect(bt_auto_connect);

         btLeService.setLogWriter(new BluetoothLeService.LogWriter() {
              @Override
              public void write(String txt) { writeLog(txt); }
         });


         new MyThread() {
             public void run() {
               sleep(500); // delay really necessary ?
               if (btLeService != null &&
                   btLeService.connect(address,bt_connect_id))
                 writeLog("btLeService.connect(" + bt_type + "): OK ");
               else
                 writeLog("btLeService.connect(" + bt_type + "): FAILED ");
             }
          }.start();

       }
   
       @Override
       public void onServiceDisconnected(ComponentName cName) {
         writeLog("onServiceDisconnected");
         if (btLeService != null) btLeService.close();
         btLeService = null;
       }
     };


     writeLog("bind LE Services");

     Intent intent = null;

     if (bt_type.equals("hrt"))
       intent = new Intent(context,HrateLeService.class);
     else
       if (bt_type.equals("pwr"))
         intent = new Intent(context,PowerLeService.class);
       else
         if (bt_type.equals("cad"))
           intent = new Intent(context,CadenceLeService.class);
         else
           if (bt_type.equals("tmp"))
             intent = new Intent(context,TemperatureLeService.class);
           else
             if (bt_type.equals("fit"))
               intent = new Intent(context,FitnessLeService.class);
             else
               if (bt_type.equals("any"))
                 intent = new Intent(context,BluetoothLeService.class);

     context.bindService(intent,btServiceConnection,Context.BIND_AUTO_CREATE);

   }


   public void disconnect() 
   { 
     writeLog("btLeService.disconnect " + bt_type);

     if (btLeService == null) {
       writeLog("btLeService == null");
       return;
     }

     try { btLeService.disconnect(); 
     } catch(Exception ex) { writeLog(ex.toString()); }

/* 
     // dont do that (close will not be called)
     btLeService = null;
*/

     writeLog("unbind btServiceConnection");

     try {context.unbindService(btServiceConnection);
     } catch(Exception ex) { writeLog(ex.toString()); }

     btServiceConnection = null;

     update_data(null);
   }


   public void close() {
      writeLog("BLUETOOTH: CLOSE");
      if (btLeService != null) btLeService.close();
      btLeService = null;
   }

  
}
