/*
 * Copyright 2024 Google LLC
 *
 * 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.
 */

#include "api.h"

#include <stddef.h>
#include <stdbool.h>

#include "app_db.h"
#include "app_glance_db.h"
#include "contacts_db.h"
#include "health_db.h"
#include "ios_notif_pref_db.h"
#include "notif_db.h"
#include "pin_db.h"
#include "prefs_db.h"
#include "reminder_db.h"
#include "watch_app_prefs_db.h"
#include "weather_db.h"

#include "kernel/events.h"
#include "kernel/pbl_malloc.h"
#include "system/logging.h"

typedef struct {
  BlobDBInitImpl init;
  BlobDBInsertImpl insert;
  BlobDBGetLenImpl get_len;
  BlobDBReadImpl read;
  BlobDBDeleteImpl del;
  BlobDBFlushImpl flush;
  BlobDBIsDirtyImpl is_dirty;
  BlobDBGetDirtyListImpl get_dirty_list;
  BlobDBMarkSyncedImpl mark_synced;
  bool disabled;
} BlobDB;

static const BlobDB s_blob_dbs[NumBlobDBs] = {
  [BlobDBIdPins] = {
    .init = pin_db_init,
    .insert = pin_db_insert,
    .get_len = pin_db_get_len,
    .read = pin_db_read,
    .del = pin_db_delete,
    .flush = pin_db_flush,
    .is_dirty = pin_db_is_dirty,
    .get_dirty_list = pin_db_get_dirty_list,
    .mark_synced = pin_db_mark_synced,
  },
  [BlobDBIdApps] = {
    .init = app_db_init,
    .insert = app_db_insert,
    .get_len = app_db_get_len,
    .read = app_db_read,
    .del = app_db_delete,
    .flush = app_db_flush,
  },
  [BlobDBIdReminders] = {
    .init = reminder_db_init,
    .insert = reminder_db_insert,
    .get_len = reminder_db_get_len,
    .read = reminder_db_read,
    .del = reminder_db_delete,
    .flush = reminder_db_flush,
    .is_dirty = reminder_db_is_dirty,
    .get_dirty_list = reminder_db_get_dirty_list,
    .mark_synced = reminder_db_mark_synced,
  },
  [BlobDBIdNotifs] = {
    .init = notif_db_init,
    .insert = notif_db_insert,
    .get_len = notif_db_get_len,
    .read = notif_db_read,
    .del = notif_db_delete,
    .flush = notif_db_flush,
  },
  [BlobDBIdWeather] = {
#if CAPABILITY_HAS_WEATHER
    .init = weather_db_init,
    .insert = weather_db_insert,
    .get_len = weather_db_get_len,
    .read = weather_db_read,
    .del = weather_db_delete,
    .flush = weather_db_flush,
#else
    .disabled = true,
#endif
  },
  [BlobDBIdiOSNotifPref] = {
    .init = ios_notif_pref_db_init,
    .insert = ios_notif_pref_db_insert,
    .get_len = ios_notif_pref_db_get_len,
    .read = ios_notif_pref_db_read,
    .del = ios_notif_pref_db_delete,
    .flush = ios_notif_pref_db_flush,
    .is_dirty = ios_notif_pref_db_is_dirty,
    .get_dirty_list = ios_notif_pref_db_get_dirty_list,
    .mark_synced = ios_notif_pref_db_mark_synced,
  },
  [BlobDBIdPrefs] = {
    .init = prefs_db_init,
    .insert = prefs_db_insert,
    .get_len = prefs_db_get_len,
    .read = prefs_db_read,
    .del = prefs_db_delete,
    .flush = prefs_db_flush,
  },
  [BlobDBIdContacts] = {
#if !PLATFORM_TINTIN
    .init = contacts_db_init,
    .insert = contacts_db_insert,
    .get_len = contacts_db_get_len,
    .read = contacts_db_read,
    .del = contacts_db_delete,
    .flush = contacts_db_flush,
#else
    // Disabled on tintin for code savings
    .disabled = true,
#endif
  },
  [BlobDBIdWatchAppPrefs] = {
#if !PLATFORM_TINTIN
    .init = watch_app_prefs_db_init,
    .insert = watch_app_prefs_db_insert,
    .get_len = watch_app_prefs_db_get_len,
    .read = watch_app_prefs_db_read,
    .del = watch_app_prefs_db_delete,
    .flush = watch_app_prefs_db_flush,
#else
    // Disabled on tintin for code savings
    .disabled = true,
#endif
  },
  [BlobDBIdHealth] = {
#if CAPABILITY_HAS_HEALTH_TRACKING
    .init = health_db_init,
    .insert = health_db_insert,
    .get_len = health_db_get_len,
    .read = health_db_read,
    .del = health_db_delete,
    .flush = health_db_flush,
#else
    .disabled = true,
#endif
  },
  [BlobDBIdAppGlance] = {
#if CAPABILITY_HAS_APP_GLANCES
    .init = app_glance_db_init,
    .insert = app_glance_db_insert,
    .get_len = app_glance_db_get_len,
    .read = app_glance_db_read,
    .del = app_glance_db_delete,
    .flush = app_glance_db_flush,
#else
    .disabled = true,
#endif
  },
};

static bool prv_db_valid(BlobDBId db_id) {
  return (db_id < NumBlobDBs) && (!s_blob_dbs[db_id].disabled);

}

void blob_db_event_put(BlobDBEventType type, BlobDBId db_id, const uint8_t *key, int key_len) {
  // copy key for event
  uint8_t *key_bytes = NULL;
  if (key_len > 0) {
    key_bytes = kernel_malloc(key_len);
    memcpy(key_bytes, key, key_len);
  }

  PebbleEvent e = {
    .type = PEBBLE_BLOBDB_EVENT,
    .blob_db = {
      .db_id = db_id,
      .type = type,
      .key = key_bytes,
      .key_len = (uint8_t)key_len,
    }
  };
  event_put(&e);
}

void blob_db_init_dbs(void) {
  const BlobDB *db = s_blob_dbs;
  for (int i = 0; i < NumBlobDBs; ++i, ++db) {
    if (db->init) {
      db->init();
    }
  }
}

void blob_db_get_dirty_dbs(uint8_t *ids, uint8_t *num_ids) {
  const BlobDB *db = s_blob_dbs;
  *num_ids = 0;
  for (uint8_t i = 0; i < NumBlobDBs; ++i, ++db) {
    bool is_dirty = false;
    if (db->is_dirty && (db->is_dirty(&is_dirty) == S_SUCCESS) && is_dirty) {
      ids[*num_ids] = i;
      *num_ids += 1;
    }
  }
}

status_t blob_db_insert(BlobDBId db_id,
    const uint8_t *key, int key_len, const uint8_t *val, int val_len) {
  if (!prv_db_valid(db_id)) {
    return E_RANGE;
  }

  const BlobDB *db = &s_blob_dbs[db_id];
  if (db->insert) {
    status_t rv = db->insert(key, key_len, val, val_len);
    if (rv == S_SUCCESS) {
      blob_db_event_put(BlobDBEventTypeInsert, db_id, key, key_len);
    }

    return rv;
  }

  return E_INVALID_OPERATION;
}

int blob_db_get_len(BlobDBId db_id,
    const uint8_t *key, int key_len) {
  if (!prv_db_valid(db_id)) {
    return E_RANGE;
  }

  const BlobDB *db = &s_blob_dbs[db_id];
  if (db->get_len) {
    return db->get_len(key, key_len);
  }

  return E_INVALID_OPERATION;
}

status_t blob_db_read(BlobDBId db_id,
    const uint8_t *key, int key_len, uint8_t *val_out, int val_len) {
  if (!prv_db_valid(db_id)) {
    return E_RANGE;
  }

  const BlobDB *db = &s_blob_dbs[db_id];
  if (db->read) {
    return db->read(key, key_len, val_out, val_len);
  }

  return E_INVALID_OPERATION;
}

status_t blob_db_delete(BlobDBId db_id,
    const uint8_t *key, int key_len) {
  if (!prv_db_valid(db_id)) {
    return E_RANGE;
  }

  const BlobDB *db = &s_blob_dbs[db_id];
  if (db->del) {
    status_t rv = db->del(key, key_len);
    if (rv == S_SUCCESS) {
      blob_db_event_put(BlobDBEventTypeDelete, db_id, key, key_len);
    }
    return rv;
  }

  return E_INVALID_OPERATION;
}

status_t blob_db_flush(BlobDBId db_id) {
  if (!prv_db_valid(db_id)) {
    return E_RANGE;
  }

  const BlobDB *db = &s_blob_dbs[db_id];
  if (db->flush) {
    status_t rv = db->flush();
    if (rv == S_SUCCESS) {
      PBL_LOG(LOG_LEVEL_INFO, "Flushing BlobDB with Id %d", db_id);
      blob_db_event_put(BlobDBEventTypeFlush, db_id, NULL, 0);
    }
    return rv;
  }

  return E_INVALID_OPERATION;
}

BlobDBDirtyItem *blob_db_get_dirty_list(BlobDBId db_id) {
  if (!prv_db_valid(db_id)) {
    return NULL;
  }

  const BlobDB *db = &s_blob_dbs[db_id];
  if (db->get_dirty_list) {
    return db->get_dirty_list();
  }

  return NULL;
}

status_t blob_db_mark_synced(BlobDBId db_id, uint8_t *key, int key_len) {
  if (!prv_db_valid(db_id)) {
    return E_RANGE;
  }

  const BlobDB *db = &s_blob_dbs[db_id];
  if (db->mark_synced) {
    status_t rv = db->mark_synced(key, key_len);
    // TODO event?
    return rv;
  }

  return E_INVALID_OPERATION;
}
