/*
 * 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 "resource.h"
#include "resource_storage.h"
#include "resource_storage_builtin.h"
#include "resource_storage_flash.h"

#include "process_management/app_manager.h"
#include "drivers/flash.h"
#include "flash_region/flash_region.h"
#include "kernel/pbl_malloc.h"
#include "os/mutex.h"
#include "services/normal/process_management/app_storage.h"
#include "system/logging.h"
#include "system/passert.h"

// TODO: this may be replaced once apps become more dynamic

typedef struct {
  ListNode list_node;
  uint32_t id;
  ResourceStoreEntry stored_resource;
} CachedResource;

PebbleRecursiveMutex *s_resource_mutex = NULL;

static CachedResource *s_resource_list = NULL;

static bool prv_resource_filter(ListNode *found_node, void *data) {
  CachedResource *resource = (CachedResource *)found_node;
  uint32_t resource_id = (uint32_t)data;

  return (resource->id == resource_id);
}

static void prv_get_resource(ResAppNum app_num, uint32_t id, ResourceStoreEntry *entry) {
  if (id < 1) {
    *entry = (ResourceStoreEntry){0};
    return;
  }

  mutex_lock_recursive(s_resource_mutex);

  ListNode *node;
  if (app_num == SYSTEM_APP &&
     (node = list_find((ListNode *)s_resource_list, prv_resource_filter, (void *)(uintptr_t)id))) {
    mutex_unlock_recursive(s_resource_mutex);
    *entry = ((CachedResource *)node)->stored_resource;
    return;
  }

  resource_storage_get_resource(app_num, id, entry);
  mutex_unlock_recursive(s_resource_mutex);
}

//! initialize components needed for one apps resources
bool resource_init_app(ResAppNum app_num, const ResourceVersion *expected_version) {
  // resource_id is ignored in this case, so we set it to 0
  mutex_lock_recursive(s_resource_mutex);
  bool rv = resource_storage_check(app_num, 0, expected_version);
  mutex_unlock_recursive(s_resource_mutex);
  return rv;
}

void resource_init(void) {
  // see if there's a system bank waiting to be loaded
  resource_storage_init();

  s_resource_mutex = mutex_create_recursive();
}

uint32_t resource_get_and_cache(ResAppNum app_num, uint32_t resource_id) {
  PBL_ASSERTN(app_num == SYSTEM_APP);
  // get from resource store
  mutex_lock_recursive(s_resource_mutex);
  ResourceStoreEntry res;
  resource_storage_get_resource(app_num, resource_id, &res);
  if (res.id < 1) {
    mutex_unlock_recursive(s_resource_mutex);
    return 0;
  }

  // check if we already have something in cache for this resource
  CachedResource *cached_resource = (CachedResource *)list_find((ListNode *)s_resource_list,
      prv_resource_filter, (void *)(uintptr_t)resource_id);
  if (cached_resource == NULL) {
    cached_resource = kernel_malloc_check(sizeof(CachedResource));
    *cached_resource = (CachedResource){};
    cached_resource->id = resource_id;
    s_resource_list = (CachedResource *)list_prepend((ListNode *)s_resource_list,
        (ListNode *)cached_resource);
  }
  cached_resource->stored_resource = res;

  mutex_unlock_recursive(s_resource_mutex);
  return resource_id;
}

size_t resource_load_byte_range_system(ResAppNum app_num, uint32_t resource_id,
    uint32_t offset, uint8_t *buffer, size_t num_bytes) {
  PBL_ASSERTN(buffer);

  if (!num_bytes) {
    return 0;
  }

  mutex_lock_recursive(s_resource_mutex);
  ResourceStoreEntry resource;
  prv_get_resource(app_num, resource_id, &resource);
  if (resource.id < 1) {
    mutex_unlock_recursive(s_resource_mutex);
    return 0;
  }

  if (offset + num_bytes > resource.length) {
    if (offset >= resource.length) {
      // Can't recover from trying to read from beyond the resource. Read nothing.
      mutex_unlock_recursive(s_resource_mutex);
      return 0;
    }
    // We want to stop the FW from doing this, so we added an assert
    // but in the name of backwards compatibility, we let the app misbehave
    num_bytes = resource.length - offset;
    PBL_LOG(LOG_LEVEL_DEBUG, "Tried to read past end of resource, reading %d bytes",
            (int)num_bytes);
  }

  size_t bytes_read = resource_storage_read(&resource, offset, buffer, num_bytes);
  mutex_unlock_recursive(s_resource_mutex);
  return bytes_read;
}

size_t resource_size(ResAppNum app_num, uint32_t resource_id) {
  mutex_lock_recursive(s_resource_mutex);
  ResourceStoreEntry resource;
  prv_get_resource(app_num, resource_id, &resource);
  mutex_unlock_recursive(s_resource_mutex);
  return resource.length;
}

bool resource_bytes_are_readonly(void *bytes) {
  return resource_storage_builtin_bytes_are_readonly(bytes) ||
         resource_storage_flash_bytes_are_readonly(bytes);
}

const uint8_t *resource_get_readonly_bytes(ResAppNum app_num, uint32_t resource_id,
                                           size_t *num_bytes_out, bool has_privileged_access) {
  // we don't support memory-mapping for resources that don't belong to the system
  if (app_num != SYSTEM_APP) {
    return NULL;
  }

  mutex_lock_recursive(s_resource_mutex);

  // FIXME PBL-28781: This operation touches flash. Even though this is the cleanest approach
  // to detect if the resource is a builtin, it is a slow one. We should instead only search
  // in the builtin table for the resource_ids and if there are no matches, bail early.
  ResourceStoreEntry resource;
  prv_get_resource(app_num, resource_id, &resource);
  mutex_unlock_recursive(s_resource_mutex);

  if (num_bytes_out) {
    *num_bytes_out = resource.length;
  }

  return resource.impl->readonly_bytes(&resource, has_privileged_access);
}

ResourceVersion resource_get_version(ResAppNum app_num, uint32_t resource_id) {
  mutex_lock_recursive(s_resource_mutex);
  ResourceVersion v = resource_storage_get_version(app_num, resource_id);
  mutex_unlock_recursive(s_resource_mutex);
  return v;
}

ResourceVersion resource_get_system_version(void) {
  return resource_get_version(0, 0);
}

bool resource_is_valid(ResAppNum app_num, uint32_t resource_id) {
  mutex_lock_recursive(s_resource_mutex);
  bool rv = resource_storage_check(app_num, resource_id, NULL /* No expected version */);
  if (rv) {
    ResourceStoreEntry entry;
    prv_get_resource(app_num, resource_id, &entry);
    rv = (entry.id != 0);
  }
  mutex_unlock_recursive(s_resource_mutex);
  return rv;
}

bool resource_version_matches(const ResourceVersion *v1, const ResourceVersion *v2) {
  return (v1->crc == v2->crc);
}

