/*
 * BRLTTY - A background process providing access to the console screen (when in
 *          text mode) for a blind person using a refreshable braille display.
 *
 * Copyright (C) 1995-2022 by The BRLTTY Developers.
 *
 * BRLTTY comes with ABSOLUTELY NO WARRANTY.
 *
 * This is free software, placed under the terms of the
 * GNU Lesser General Public License, as published by the Free Software
 * Foundation; either version 2.1 of the License, or (at your option) any
 * later version. Please see the file LICENSE-LGPL for details.
 *
 * Web Page: http://brltty.app/
 *
 * This software is maintained by Dave Mielke <dave@mielke.cc>.
 */

#include "prologue.h"

#include <string.h>
#include <errno.h>

#include "log.h"
#include "strfmt.h"
#include "parameters.h"
#include "timing.h"
#include "async_wait.h"
#include "parse.h"
#include "device.h"
#include "queue.h"
#include "io_bluetooth.h"
#include "bluetooth_internal.h"

static int
bthDiscoverSerialPortChannel (uint8_t *channel, BluetoothConnectionExtension *bcx, int timeout) {
  static const uint8_t uuid[] = {
    0X00, 0X00, 0X11, 0X01,
    0X00, 0X00,
    0X10, 0X00,
    0X80, 0X00,
    0X00, 0X80, 0X5F, 0X9B, 0X34, 0XFB
  };

  logMessage(LOG_CATEGORY(BLUETOOTH_IO), "discovering serial port channel");
  int discovered = bthDiscoverChannel(channel, bcx, uuid, sizeof(uuid), timeout);

  if (discovered) {
    logMessage(LOG_CATEGORY(BLUETOOTH_IO), "serial port channel discovered: %u", *channel);
  } else {
    logMessage(LOG_CATEGORY(BLUETOOTH_IO), "serial port channel not discovered");
  }

  return discovered;
}

static void
bthLogChannel (uint8_t channel) {
  logMessage(LOG_CATEGORY(BLUETOOTH_IO), "RFCOMM channel: %u", channel);
}

typedef struct {
  uint64_t address;
  char *name;
  int error;
  unsigned paired:1;
} BluetoothDeviceEntry;

static void
bthDeallocateDeviceEntry (void *item, void *data) {
  BluetoothDeviceEntry *device = item;

  if (device->name) free(device->name);
  free(device);
}

static Queue *
bthCreateDeviceQueue (void *data) {
  return newQueue(bthDeallocateDeviceEntry, NULL);
}

static Queue *
bthGetDeviceQueue (int create) {
  static Queue *devices = NULL;

  return getProgramQueue(&devices, "bluetooth-device-queue", create,
                         bthCreateDeviceQueue, NULL);
}

static int
bthTestDeviceAddress (const void *item, void *data) {
  const BluetoothDeviceEntry *device = item;
  const uint64_t *address = data;

  return device->address == *address;
}

static BluetoothDeviceEntry *
bthGetDeviceEntry (uint64_t address, int add) {
  Queue *devices = bthGetDeviceQueue(add);

  if (devices) {
    BluetoothDeviceEntry *device = findItem(devices, bthTestDeviceAddress, &address);
    if (device) return device;

    if (add) {
      if ((device = malloc(sizeof(*device)))) {
        device->address = address;
        device->name = NULL;
        device->error = 0;
        device->paired = 0;

        if (enqueueItem(devices, device)) return device;
        free(device);
      } else {
        logMallocError();
      }
    }
  }

  return NULL;
}

static int
bthSetDeviceName (BluetoothDeviceEntry *device, const char *name) {
  if (name && *name) {
    char *copy = strdup(name);

    if (copy) {
      if (device->name) free(device->name);
      device->name = copy;
      return 1;
    } else {
      logMallocError();
    }
  }

  return 0;
}

static inline const char *
bthGetPairedKeyword (int state) {
  return getFlagKeywordYesNo(state);
}

static int
bthRememberDiscoveredDevice (const DiscoveredBluetoothDevice *device, void *data) {
  logMessage(LOG_CATEGORY(BLUETOOTH_IO),
             "remember discovered device: "
             "Addr:%012" PRIX64 " Paired:%s Name:%s",
             device->address, bthGetPairedKeyword(device->paired), device->name);

  BluetoothDeviceEntry *entry = bthGetDeviceEntry(device->address, 1);

  if (entry) {
    bthSetDeviceName(entry, device->name);
    entry->paired = device->paired;
  }

  return 0;
}

static int bluetoothDevicesDiscovered = 0;

static void
bthDiscoverDevices (void) {
  if (!bluetoothDevicesDiscovered) {
    logMessage(LOG_CATEGORY(BLUETOOTH_IO), "begin device discovery");
    bthProcessDiscoveredDevices(bthRememberDiscoveredDevice, NULL);
    bluetoothDevicesDiscovered = 1;
    logMessage(LOG_CATEGORY(BLUETOOTH_IO), "end device discovery");
  }
}

void
bthForgetDevices (void) {
  Queue *devices = bthGetDeviceQueue(0);

  if (devices) deleteElements(devices);
  bluetoothDevicesDiscovered = 0;
}

static int
bthRememberConnectError (uint64_t address, int value) {
  BluetoothDeviceEntry *device = bthGetDeviceEntry(address, 1);
  if (!device) return 0;

  device->error = value;
  return 1;
}

static int
bthRecallConnectError (uint64_t address, int *value) {
  BluetoothDeviceEntry *device = bthGetDeviceEntry(address, 0);
  if (!device) return 0;
  if (!device->error) return 0;

  *value = device->error;
  return 1;
}

void
bthInitializeConnectionRequest (BluetoothConnectionRequest *request) {
  memset(request, 0, sizeof(*request));
  request->driver = NULL;
  request->address = 0;
  request->timeout = BLUETOOTH_CHANNEL_CONNECT_TIMEOUT;
  request->channel = 0;
  request->discover = 0;
}

typedef enum {
  BTH_PARM_ADDRESS,
  BTH_PARM_NAME,
  BTH_PARM_CHANNEL,
  BTH_PARM_DISCOVER,
  BTH_PARM_TIMEOUT
} BluetoothDeviceParameter;

static const char *const bthDeviceParameterNames[] = {
  "address",
  "name",
  "channel",
  "discover",
  "timeout",
  NULL
};

static char **
bthGetDeviceParameters (const char *identifier) {
  if (!identifier) identifier = "";
  return getDeviceParameters(bthDeviceParameterNames, identifier);
}

int
bthParseAddress (uint64_t *address, const char *string) {
  const char *character = string;
  unsigned int counter = BDA_SIZE;
  char delimiter = 0;
  *address = 0;

  while (*character) {
    long unsigned int value;

    {
      const char *start = character;
      char *end;

      value = strtoul(character, &end, 0X10);
      if ((end - start) != 2) break;
      character = end;
    }

    if (value > UINT8_MAX) break;
    *address <<= 8;
    *address |= value;

    if (!--counter) {
      if (*character) break;
      return 1;
    }

    if (!*character) break;

    if (!delimiter) {
      delimiter = *character;
      if ((delimiter != ':') && (delimiter != '-')) break;
    } else if (*character != delimiter) {
      break;
    }

    character += 1;
  }

  logMessage(LOG_ERR, "invalid Bluetooth device address: %s", string);
  errno = EINVAL;
  return 0;
}

int
bthParseChannelNumber (uint8_t *channel, const char *string) {
  if (*string) {
    unsigned int value;

    if (isUnsignedInteger(&value, string)) {
      if ((value > 0) && (value < 0X1F)) {
        *channel = value;
        return 1;
      }
    }
  }

  logMessage(LOG_WARNING, "invalid RFCOMM channel number: %s", string);
  return 0;
}

STR_BEGIN_FORMATTER(bthFormatAddress, uint64_t address)
  uint8_t bytes[6];
  size_t count = ARRAY_COUNT(bytes);
  unsigned int index = count;

  while (index > 0) {
    bytes[--index] = address & 0XFF;
    address >>= 8;
  }

  while (index < count) {
    if (index > 0) STR_PRINTF("%c", ':');
    STR_PRINTF("%02X", bytes[index++]);
  }
STR_END_FORMATTER

static const BluetoothNameEntry *
bthGetNameEntry (const char *name) {
  if (name && *name) {
    const BluetoothNameEntry *entry = bluetoothNameTable;

    while (entry->namePrefix) {
      if (strncmp(name, entry->namePrefix, strlen(entry->namePrefix)) == 0) {
        return entry;
      }

      entry += 1;
    }
  }

  return NULL;
}

typedef struct {
  struct {
    const char *address;
    size_t length;
  } name;

  struct {
    const char *address;
    size_t length;
  } driver;
} GetDeviceAddressData;

static int
bthTestDeviceName (const void *item, void *data) {
  const BluetoothDeviceEntry *device = item;
  const GetDeviceAddressData *gda = data;

  logMessage(LOG_CATEGORY(BLUETOOTH_IO),
             "testing device: Addr:%012" PRIX64 " Paired:%s Name:%s",
             device->address, bthGetPairedKeyword(device->paired), device->name
  );

  if (!device->paired) {
    logMessage(LOG_CATEGORY(BLUETOOTH_IO), "not paired");
    return 0;
  }

  if (gda->name.length) {
    if (strncmp(device->name, gda->name.address, gda->name.length) != 0) {
      logMessage(LOG_CATEGORY(BLUETOOTH_IO), "ineligible name");
      return 0;
    }
  }

  const BluetoothNameEntry *name = bthGetNameEntry(device->name);
  if (!name) {
    logMessage(LOG_CATEGORY(BLUETOOTH_IO), "unrecognized name");
    return 0;
  }

  if (gda->driver.length) {
    const char *const *code = name->driverCodes;

    while (*code) {
      if (strncmp(*code, gda->driver.address, gda->driver.length) == 0) {
        logMessage(LOG_CATEGORY(BLUETOOTH_IO), "found");
        return 1;
      }

      code += 1;
    }

    logMessage(LOG_CATEGORY(BLUETOOTH_IO), "ineligible driver");
    return 0;
  }

  logMessage(LOG_CATEGORY(BLUETOOTH_IO), "driver not specified");
  return 0;
}

static int
bthGetDeviceAddress (uint64_t *address, char **parameters, const char *driver) {
  {
    const char *parameter = parameters[BTH_PARM_ADDRESS];

    if (parameter && *parameter) {
      return bthParseAddress(address, parameter);
    }
  }

  {
    bthDiscoverDevices();
    Queue *devices = bthGetDeviceQueue(0);

    if (devices) {
      const char *name = parameters[BTH_PARM_NAME];

      GetDeviceAddressData gda = {
        .name = {
          .address = name,
          .length = name? strlen(name): 0
        },

        .driver = {
          .address = driver,
          .length = driver? strlen(driver): 0
        }
      };

      if (gda.driver.length) {
        logMessage(LOG_CATEGORY(BLUETOOTH_IO), "begin device search");
        const BluetoothDeviceEntry *device = findItem(devices, bthTestDeviceName, &gda);
        logMessage(LOG_CATEGORY(BLUETOOTH_IO), "end device search");

        if (device) {
          *address = device->address;
          return 1;
        }
      }
    }
  }

  return 0;
}

static int
bthProcessTimeoutParameter (BluetoothConnectionRequest *request, const char *parameter) {
  int seconds;

  if (!parameter) return 1;
  if (!*parameter) return 1;

  if (isInteger(&seconds, parameter)) {
    if ((seconds > 0) && (seconds < 60)) {
      request->timeout = seconds * MSECS_PER_SEC;
      return 1;
    }
  }

  logMessage(LOG_ERR, "invalid Bluetooth connection timeout: %s", parameter);
  return 0;
}

static int
bthProcessChannelParameter (BluetoothConnectionRequest *request, const char *parameter) {
  uint8_t channel;

  if (!parameter) return 1;
  if (!*parameter) return 1;

  if (bthParseChannelNumber(&channel, parameter)) {
    request->channel = channel;
    request->discover = 0;
    return 1;
  }

  return 0;
}

static int
bthProcessDiscoverParameter (BluetoothConnectionRequest *request, const char *parameter) {
  unsigned int flag;

  if (!parameter) return 1;
  if (!*parameter) return 1;

  if (validateYesNo(&flag, parameter)) {
    request->discover = flag;
    return 1;
  }

  logMessage(LOG_ERR, "invalid discover option: %s", parameter);
  return 0;
}

int
bthApplyParameters (BluetoothConnectionRequest *request, const char *identifier) {
  char **parameters = bthGetDeviceParameters(identifier);
  if (!parameters) return 0;

  int ok = 1;
  if (!bthProcessChannelParameter(request, parameters[BTH_PARM_CHANNEL])) ok = 0;
  if (!bthProcessDiscoverParameter(request, parameters[BTH_PARM_DISCOVER])) ok = 0;
  if (!bthProcessTimeoutParameter(request, parameters[BTH_PARM_TIMEOUT])) ok = 0;
  if (!bthGetDeviceAddress(&request->address, parameters, request->driver)) ok = 0;

  deallocateStrings(parameters);
  return ok;
}

BluetoothConnection *
bthOpenConnection (const BluetoothConnectionRequest *request) {
  BluetoothConnection *connection;

  if ((connection = malloc(sizeof(*connection)))) {
    memset(connection, 0, sizeof(*connection));
    connection->address = request->address;
    connection->channel = request->channel;

    if ((connection->extension = bthNewConnectionExtension(connection->address))) {
      int alreadyTried = 0;

      {
        int value;

        if (bthRecallConnectError(connection->address, &value)) {
          errno = value;
          alreadyTried = 1;
        }
      }

      if (!alreadyTried) {
        if (request->discover) bthDiscoverSerialPortChannel(&connection->channel, connection->extension, request->timeout);
        bthLogChannel(connection->channel);

        {
          TimePeriod period;
          startTimePeriod(&period, BLUETOOTH_CHANNEL_BUSY_RETRY_TIMEOUT);

          while (1) {
            if (bthOpenChannel(connection->extension, connection->channel, request->timeout)) {
              return connection;
            }

            if (afterTimePeriod(&period, NULL)) break;
            if (errno != EBUSY) break;
            asyncWait(BLUETOOTH_CHANNEL_BUSY_RETRY_INTERVAL);
          }
        }

        bthRememberConnectError(connection->address, errno);
      }

      bthReleaseConnectionExtension(connection->extension);
    }

    free(connection);
  } else {
    logMallocError();
  }

  return NULL;
}

void
bthCloseConnection (BluetoothConnection *connection) {
  bthReleaseConnectionExtension(connection->extension);
  free(connection);
}

const char *
bthMakeConnectionIdentifier (BluetoothConnection *connection, char *buffer, size_t size) {
  size_t length;
  STR_BEGIN(buffer, size);
  STR_PRINTF("%s%c", BLUETOOTH_DEVICE_QUALIFIER, PARAMETER_QUALIFIER_CHARACTER);

  {
    uint64_t address = bthGetAddress(connection);
    STR_PRINTF("%s%c", bthDeviceParameterNames[BTH_PARM_ADDRESS], PARAMETER_ASSIGNMENT_CHARACTER);
    STR_FORMAT(bthFormatAddress, address);
    STR_PRINTF("%c", DEVICE_PARAMETER_SEPARATOR);
  }

  {
    uint8_t channel = bthGetChannel(connection);

    if (channel) {
      STR_PRINTF(
        "%s%c%u%c",
        bthDeviceParameterNames[BTH_PARM_CHANNEL],
        PARAMETER_ASSIGNMENT_CHARACTER,
        channel,
        DEVICE_PARAMETER_SEPARATOR
      );
    }
  }

  length = STR_LENGTH;
  STR_END;

  {
    char *last = &buffer[length] - 1;
    if (*last == DEVICE_PARAMETER_SEPARATOR) *last = 0;
  }

  return buffer;
}

uint64_t
bthGetAddress (BluetoothConnection *connection) {
  return connection->address;
}

uint8_t
bthGetChannel (BluetoothConnection *connection) {
  return connection->channel;
}

int
bthAwaitInput (BluetoothConnection *connection, int timeout) {
  return bthPollInput(connection->extension, timeout);
}

ssize_t
bthReadData (
  BluetoothConnection *connection, void *buffer, size_t size,
  int initialTimeout, int subsequentTimeout
) {
  ssize_t result = bthGetData(connection->extension, buffer, size, initialTimeout, subsequentTimeout);

  if (result > 0) logBytes(LOG_CATEGORY(BLUETOOTH_IO), "input", buffer, result);
  return result;
}

ssize_t
bthWriteData (BluetoothConnection *connection, const void *buffer, size_t size) {
  if (size > 0) logBytes(LOG_CATEGORY(BLUETOOTH_IO), "output", buffer, size);
  return bthPutData(connection->extension, buffer, size);
}

static char *
bthGetDeviceName (uint64_t address, int timeout) {
  bthDiscoverDevices();
  BluetoothDeviceEntry *device = bthGetDeviceEntry(address, 1);

  if (device) {
    if (!device->name) {
      logMessage(LOG_CATEGORY(BLUETOOTH_IO), "obtaining device name");

      if ((device->name = bthObtainDeviceName(address, timeout))) {
        logMessage(LOG_CATEGORY(BLUETOOTH_IO), "device name: %s", device->name);
      } else {
        logMessage(LOG_CATEGORY(BLUETOOTH_IO), "device name not obtained");
      }
    }

    return device->name;
  }

  return NULL;
}

char *
bthGetNameOfDevice (BluetoothConnection *connection, int timeout) {
  return bthGetDeviceName(connection->address, timeout);
}

char *
bthGetNameAtAddress (const char *address, int timeout) {
  uint64_t bda;

  if (bthParseAddress(&bda, address)) {
    return bthGetDeviceName(bda, timeout);
  }

  return NULL;
}

static struct {
  const char **table;
  unsigned int count;
  unsigned int size;
} driverCodes = {
  .table = NULL,
  .count = 0,
  .size = 0
};

static int
bthFindDriverCode (unsigned int *position, const char *code) {
  int first = 0;
  int last = driverCodes.count - 1;

  while (first <= last) {
    unsigned int current = (first + last) / 2;
    int relation = strcmp(code, driverCodes.table[current]);

    if (relation < 0) {
      last = current - 1;
    } else if (relation > 0) {
      first = current + 1;
    } else {
      *position = current;
      return 1;
    }
  }

  *position = first;
  return 0;
}

static int
bthEnsureDriverCodeTableSize (void) {
  if (driverCodes.count < driverCodes.size) return 1;

  unsigned int newSize = driverCodes.size + 0X10;
  const char **newTable = realloc(driverCodes.table, ARRAY_SIZE(driverCodes.table, newSize));

  if (!newTable) {
    logMallocError();
    return 0;
  }

  driverCodes.table = newTable;
  driverCodes.size = newSize;
  return 1;
}

static int
bthAddDriverCode (const char *code) {
  unsigned int position;
  if (bthFindDriverCode(&position, code)) return 1;
  if (!bthEnsureDriverCodeTableSize()) return 0;

  memmove(&driverCodes.table[position+1], &driverCodes.table[position],
          ARRAY_SIZE(driverCodes.table, (driverCodes.count - position)));

  driverCodes.count += 1;
  driverCodes.table[position] = code;
  return 1;
}

static const char *const *
bthGetAllDriverCodes (void) {
  if (driverCodes.table) return driverCodes.table;
  const BluetoothNameEntry *entry = bluetoothNameTable;

  while (entry->namePrefix) {
    const char *const *code = entry->driverCodes;

    while (*code) {
      if (!bthAddDriverCode(*code)) goto failure;
      code += 1;
    }

    entry += 1;
  }

  if (bthEnsureDriverCodeTableSize()) {
    driverCodes.table[driverCodes.count] = NULL;
    return driverCodes.table;
  }

failure:
  if (driverCodes.table) {
    free(driverCodes.table);
    memset(&driverCodes, 0, sizeof(driverCodes));
  }

  static const char *const *noDriverCodes = {NULL};
  return noDriverCodes;
}

const char *const *
bthGetDriverCodes (const char *identifier, int timeout) {
  const char *const *codes = NULL;
  char **parameters = bthGetDeviceParameters(identifier);

  if (parameters) {
    uint64_t address;

    if (bthGetDeviceAddress(&address, parameters, NULL)) {
      const char *name = bthGetDeviceName(address, timeout);
      const BluetoothNameEntry *entry = bthGetNameEntry(name);
      if (entry) codes = entry->driverCodes;
    }

    deallocateStrings(parameters);
  }

  if (!codes) codes = bthGetAllDriverCodes();
  return codes;
}

int
isBluetoothDeviceIdentifier (const char **identifier) {
  return hasQualifier(identifier, BLUETOOTH_DEVICE_QUALIFIER);
}
