/******************************************************************************
 *
 *  Copyright 2022 The Android Open Source Project
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at:
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
 ******************************************************************************/

#define LOG_TAG "bt_bta_gattc"

#include <base/strings/string_number_conversions.h>
#include <bluetooth/log.h>
#include <dirent.h>
#include <sys/stat.h>

#include <string>
#include <vector>

#include "bta/gatt/bta_gattc_int.h"
#include "gatt/database.h"
#include "stack/include/gattdefs.h"
#include "types/bluetooth/uuid.h"

using namespace bluetooth;

using gatt::StoredAttribute;
using std::string;
using std::vector;

#ifdef TARGET_FLOSS
#define GATT_CACHE_PREFIX "/var/lib/bluetooth/gatt/gatt_cache_"
#define GATT_CACHE_VERSION 6

#define GATT_HASH_MAX_SIZE 30
#define GATT_HASH_PATH_PREFIX "/var/lib/bluetooth/gatt/gatt_hash_"
#define GATT_HASH_PATH "/var/lib/bluetooth/gatt"
#define GATT_HASH_FILE_PREFIX "gatt_hash_"
#else
#define GATT_CACHE_PREFIX "/data/misc/bluetooth/gatt_cache_"
#define GATT_CACHE_VERSION 6

#define GATT_HASH_MAX_SIZE 30
#define GATT_HASH_PATH_PREFIX "/data/misc/bluetooth/gatt_hash_"
#define GATT_HASH_PATH "/data/misc/bluetooth"
#define GATT_HASH_FILE_PREFIX "gatt_hash_"
#endif

// Default expired time is 7 days
#define GATT_HASH_EXPIRED_TIME 604800

static void bta_gattc_hash_remove_least_recently_used_if_possible();

static void bta_gattc_generate_cache_file_name(char* buffer, size_t buffer_len,
                                               const RawAddress& bda) {
  snprintf(buffer, buffer_len, "%s%02x%02x%02x%02x%02x%02x", GATT_CACHE_PREFIX, bda.address[0],
           bda.address[1], bda.address[2], bda.address[3], bda.address[4], bda.address[5]);
}

static void bta_gattc_generate_hash_file_name(char* buffer, size_t buffer_len,
                                              const Octet16& hash) {
  snprintf(buffer, buffer_len, "%s%s", GATT_HASH_PATH_PREFIX,
           base::HexEncode(hash.data(), 16).c_str());
}

static gatt::Database EMPTY_DB;

/*******************************************************************************
 *
 * Function         bta_gattc_load_db
 *
 * Description      Load GATT database from storage.
 *
 * Parameter        fname: input file name
 *
 * Returns          non-empty GATT database on success, empty GATT database
 *                  otherwise
 *
 ******************************************************************************/
static gatt::Database bta_gattc_load_db(const char* fname) {
  FILE* fd = fopen(fname, "rb");
  if (!fd) {
    log::error("can't open GATT cache file {} for reading, error: {}", fname, strerror(errno));
    return EMPTY_DB;
  }

  uint16_t cache_ver = 0;
  uint16_t num_attr = 0;

  if (fread(&cache_ver, sizeof(uint16_t), 1, fd) != 1) {
    log::error("can't read GATT cache version from: {}", fname);
    goto done;
  }

  if (cache_ver != GATT_CACHE_VERSION) {
    log::error("wrong GATT cache version: {}", fname);
    goto done;
  }

  if (fread(&num_attr, sizeof(uint16_t), 1, fd) != 1) {
    log::error("can't read number of GATT attributes: {}", fname);
    goto done;
  }

  {
    std::vector<StoredAttribute> attr(num_attr);

    if (fread(attr.data(), sizeof(StoredAttribute), num_attr, fd) != num_attr) {
      log::error("can't read GATT attributes: {}", fname);
      goto done;
    }
    fclose(fd);

    bool success = false;
    gatt::Database result = gatt::Database::Deserialize(attr, &success);
    return success ? result : EMPTY_DB;
  }

done:
  fclose(fd);
  return EMPTY_DB;
}

/*******************************************************************************
 *
 * Function         bta_gattc_cache_load
 *
 * Description      Load GATT cache from storage for server.
 *
 * Parameter        bd_address: remote device address
 *
 * Returns          non-empty GATT database on success, empty GATT database
 *                  otherwise
 *
 ******************************************************************************/
gatt::Database bta_gattc_cache_load(const RawAddress& server_bda) {
  char fname[255] = {0};
  bta_gattc_generate_cache_file_name(fname, sizeof(fname), server_bda);
  return bta_gattc_load_db(fname);
}

/*******************************************************************************
 *
 * Function         bta_gattc_hash_load
 *
 * Description      Load GATT cache from storage for server.
 *
 * Parameter        hash: 16-byte value
 *
 * Returns          non-empty GATT database on success, empty GATT database
 *                  otherwise
 *
 ******************************************************************************/
gatt::Database bta_gattc_hash_load(const Octet16& hash) {
  char fname[255] = {0};
  bta_gattc_generate_hash_file_name(fname, sizeof(fname), hash);
  return bta_gattc_load_db(fname);
}

void StoredAttribute::SerializeStoredAttribute(const StoredAttribute& attr,
                                               std::vector<uint8_t>& bytes) {
  size_t original_size = bytes.size();
  // handle
  bytes.push_back(attr.handle & 0xff);
  bytes.push_back(attr.handle >> 8);
  auto uuid = attr.type.To128BitBE();
  bytes.insert(bytes.cend(), uuid.cbegin(), uuid.cend());

  if (attr.type.Is16Bit()) {
    switch (attr.type.As16Bit()) {
      /* primary or secondary service definition */
      case GATT_UUID_PRI_SERVICE:
      case GATT_UUID_SEC_SERVICE:
        uuid = attr.value.service.uuid.To128BitBE();
        bytes.insert(bytes.cend(), uuid.cbegin(), uuid.cend());
        bytes.push_back(attr.value.service.end_handle & 0xff);
        bytes.push_back(attr.value.service.end_handle >> 8);
        break;
      case GATT_UUID_INCLUDE_SERVICE:
        /* included service definition */
        bytes.push_back(attr.value.included_service.handle & 0xff);
        bytes.push_back(attr.value.included_service.handle >> 8);
        bytes.push_back(attr.value.included_service.end_handle & 0xff);
        bytes.push_back(attr.value.included_service.end_handle >> 8);
        uuid = attr.value.included_service.uuid.To128BitBE();
        bytes.insert(bytes.cend(), uuid.cbegin(), uuid.cend());
        break;
      case GATT_UUID_CHAR_DECLARE:
        /* characteristic definition */
        bytes.push_back(attr.value.characteristic.properties);
        bytes.push_back(0);  // Padding byte
        bytes.push_back(attr.value.characteristic.value_handle & 0xff);
        bytes.push_back(attr.value.characteristic.value_handle >> 8);
        uuid = attr.value.characteristic.uuid.To128BitBE();
        bytes.insert(bytes.cend(), uuid.cbegin(), uuid.cend());
        break;
      case GATT_UUID_CHAR_EXT_PROP:
        /* for descriptor we store value only for
         * «Characteristic Extended Properties» */
        bytes.push_back(attr.value.characteristic_extended_properties & 0xff);
        bytes.push_back(attr.value.characteristic_extended_properties >> 8);
        break;
      default:
        // log::verbose("Unhandled type UUID 0x{:04x}", attr.type.As16Bit());
        break;
    }
  }
  // padding
  for (size_t i = bytes.size() - original_size; i < StoredAttribute::kSizeOnDisk; i++) {
    bytes.push_back(0);
  }
}

/*******************************************************************************
 *
 * Function         bta_gattc_store_db
 *
 * Description      Storess GATT db.
 *
 * Parameter        fname: output file name
 *                  attr: attributes to save.
 *
 * Returns          true on success, false otherwise
 *
 ******************************************************************************/
static bool bta_gattc_store_db(const char* fname, const std::vector<StoredAttribute>& attr) {
  FILE* fd = fopen(fname, "wb");
  if (!fd) {
    log::error("can't open GATT cache file for writing: {}", fname);
    return false;
  }

  uint16_t cache_ver = GATT_CACHE_VERSION;
  if (fwrite(&cache_ver, sizeof(uint16_t), 1, fd) != 1) {
    log::error("can't write GATT cache version: {}", fname);
    fclose(fd);
    return false;
  }

  uint16_t num_attr = attr.size();
  if (fwrite(&num_attr, sizeof(uint16_t), 1, fd) != 1) {
    log::error("can't write GATT cache attribute count: {}", fname);
    fclose(fd);
    return false;
  }

  std::vector<uint8_t> db_bytes;
  db_bytes.reserve(num_attr * StoredAttribute::kSizeOnDisk);
  for (const auto attribute : attr) {
    StoredAttribute::SerializeStoredAttribute(attribute, db_bytes);
  }

  if (fwrite(db_bytes.data(), sizeof(uint8_t), db_bytes.size(), fd) != db_bytes.size()) {
    log::error("can't write GATT cache attributes: {}", fname);
    fclose(fd);
    return false;
  }

  fclose(fd);
  return true;
}

/*******************************************************************************
 *
 * Function         bta_gattc_cache_write
 *
 * Description      This callout function is executed by GATT when a server
 *                  cache is available to save. Before calling this API, make
 *                  sure the device is bonded. Otherwise you might get lots of
 *                  address caches for unbonded devices.
 *
 * Parameter        server_bda: server bd address of this cache belongs to
 *                  database: attributes to save.
 * Returns
 *
 ******************************************************************************/
void bta_gattc_cache_write(const RawAddress& server_bda, const gatt::Database& database) {
  char addr_file[255] = {0};
  char hash_file[255] = {0};
  Octet16 hash = database.Hash();
  bta_gattc_generate_cache_file_name(addr_file, sizeof(addr_file), server_bda);
  bta_gattc_generate_hash_file_name(hash_file, sizeof(hash_file), hash);

  bool result = bta_gattc_hash_write(hash, database);
  // Only link addr_file to hash file when hash_file is created successfully.
  if (result) {
    bta_gattc_cache_link(server_bda, hash);
  }
}

/*******************************************************************************
 *
 * Function         bta_gattc_cache_link
 *
 * Description      Link address-database file to hash-database file
 *
 * Parameter        server_bda: server bd address of this cache belongs to
 *                  hash: 16-byte value
 *
 * Returns          true on success, false otherwise
 *
 ******************************************************************************/
void bta_gattc_cache_link(const RawAddress& server_bda, const Octet16& hash) {
  char addr_file[255] = {0};
  char hash_file[255] = {0};
  bta_gattc_generate_cache_file_name(addr_file, sizeof(addr_file), server_bda);
  bta_gattc_generate_hash_file_name(hash_file, sizeof(hash_file), hash);

  unlink(addr_file);  // remove addr file first if the file exists
  if (link(hash_file, addr_file) == -1) {
    log::error("link {} to {}, errno={}", addr_file, hash_file, errno);
  }
}

/*******************************************************************************
 *
 * Function         bta_gattc_hash_write
 *
 * Description      This callout function is executed by GATT when a server
 *                  cache is available to save for specific hash.
 *
 * Parameter        hash: 16-byte value
 *                  database: gatt::Database instance.
 *
 * Returns          true on success, false otherwise
 *
 ******************************************************************************/
bool bta_gattc_hash_write(const Octet16& hash, const gatt::Database& database) {
  char fname[255] = {0};
  bta_gattc_generate_hash_file_name(fname, sizeof(fname), hash);
  bta_gattc_hash_remove_least_recently_used_if_possible();
  return bta_gattc_store_db(fname, database.Serialize());
}

/*******************************************************************************
 *
 * Function         bta_gattc_cache_reset
 *
 * Description      This callout function is executed by GATTC to reset cache in
 *                  application
 *
 * Parameter        server_bda: server bd address of this cache belongs to
 *
 * Returns          void.
 *
 ******************************************************************************/
void bta_gattc_cache_reset(const RawAddress& server_bda) {
  log::verbose("");
  char fname[255] = {0};
  bta_gattc_generate_cache_file_name(fname, sizeof(fname), server_bda);
  unlink(fname);
}

/*******************************************************************************
 *
 * Function         bta_gattc_hash_remove_least_recently_used_if_possible
 *
 * Description      When the max size reaches, find the oldest item and remove
 *                  it if possible
 *
 * Parameter
 *
 * Returns          void
 *
 ******************************************************************************/
static void bta_gattc_hash_remove_least_recently_used_if_possible() {
  std::unique_ptr<DIR, decltype(&closedir)> dirp(opendir(GATT_HASH_PATH), &closedir);
  if (dirp == nullptr) {
    log::error("open dir error, dir={}", GATT_HASH_PATH);
    return;
  }

  time_t current_time = time(NULL);
  time_t lru_time = current_time;
  size_t count = 0;
  string candidate_item;
  vector<string> expired_items;

  log::debug("<-----------Start Local Hash Cache---------->");
  dirent* dp;
  while ((dp = readdir(dirp.get())) != nullptr) {
    if (strncmp(".", dp->d_name, 1) == 0 || strncmp("..", dp->d_name, 2) == 0) {
      continue;
    }

    // pattern match: gatt_hash_
    size_t fname_len = strlen(dp->d_name);
    size_t pattern_len = strlen(GATT_HASH_FILE_PREFIX);
    if (pattern_len > fname_len) {
      continue;
    }

    // check if the file name has gatt_hash_ as prefix
    char tmp[255] = {0};
    strncpy(tmp, dp->d_name, pattern_len);
    if (strncmp(tmp, GATT_HASH_FILE_PREFIX, pattern_len) != 0) {
      continue;
    }

    // increase hash file count
    count++;

    // generate the full path, in order to get the state of the file
    snprintf(tmp, 255, "%s/%s", GATT_HASH_PATH, dp->d_name);

    struct stat buf;
    int result = lstat(tmp, &buf);
    log::debug("name={}, result={}, linknum={}, mtime={}", dp->d_name, result, buf.st_nlink,
               buf.st_mtime);

    // if hard link count of the file is 1, it means no trusted device links to
    // the inode. It is safe to be a candidate to be removed
    if (buf.st_nlink == 1) {
      if (buf.st_mtime < lru_time) {
        lru_time = buf.st_mtime;
        // Find the LRU candidate during for-loop itreation.
        candidate_item.assign(tmp);
      }

      if (buf.st_mtime + GATT_HASH_EXPIRED_TIME < current_time) {
        // Add expired item.
        expired_items.emplace_back(tmp);
      }
    }
  }
  log::debug("<-----------End Local Hash Cache------------>");

  // if the number of hash files exceeds the limit, remove the candidate item.
  if (count > GATT_HASH_MAX_SIZE && !candidate_item.empty()) {
    unlink(candidate_item.c_str());
    log::debug("delete hash file (size), name={}", candidate_item);
  }

  // If there is any file expired, also delete it.
  for (string expired_item : expired_items) {
    unlink(expired_item.c_str());
    log::debug("delete hash file (expired), name={}", expired_item);
  }
}
