/*
 * 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 "timeline_resources.h"

#include "applib/graphics/gdraw_command_private.h"
#include "applib/ui/kino/kino_reel.h"
#include "kernel/pbl_malloc.h"
#include "process_management/app_install_manager.h"
#include "resource/resource_ids.auto.h"
#include "system/logging.h"
#include "syscall/syscall.h"
#include "syscall/syscall_internal.h"
#include "system/passert.h"
#include "util/net.h"
#include "util/pack.h"
#include "util/uuid.h"

#define RESOURCE_MAX_SIZE (700)

//! Autogenerated table created by generate_resource_code.py as part of resource generation. The
//! format is:
//! tiny resource id | small resource id | large resource id
//!
//! We use uint16_t instead of uint32_t (aka ResourceId) to save space. This wont be a problem
//! until we have more than 2^16 resource ids.
extern const uint16_t g_timeline_resources[][TimelineResourceSizeCount];

T_STATIC bool prv_is_app_published_resource_valid(const AppResourceInfo *res_info) {
  // TODO: PBL-39864 Improve this code so it doesn't requiring loading/unloading a resource
  bool is_valid = true;

  KinoReel *reel = kino_reel_create_with_resource_system(res_info->res_app_num, res_info->res_id);
  const KinoReelType reel_type = kino_reel_get_type(reel);
  if ((reel_type != KinoReelTypeGBitmap) && (reel_type != KinoReelTypePDCI)) {
    APP_LOG(APP_LOG_LEVEL_ERROR, "Invalid published icon type");
    is_valid = false;
  } else {
    const size_t resource_size = kino_reel_get_data_size(reel);

    if (resource_size == 0) {
      APP_LOG(APP_LOG_LEVEL_ERROR, "Cannot determine size for published icon");
      is_valid = false;
    } else if (resource_size > RESOURCE_MAX_SIZE) {
      APP_LOG(APP_LOG_LEVEL_ERROR, "Published icon size %zuB > %dB max", resource_size,
              RESOURCE_MAX_SIZE);
      is_valid = false;
    }
  }
  kino_reel_destroy(reel);

  return is_valid;
}

T_STATIC bool prv_validate_lut(ResAppNum res_app_num) {
  uint32_t data_signature;
  if (sys_resource_load_range(res_app_num, TLUT_RESOURCE_ID, 0, (uint8_t *)&data_signature,
                              sizeof(data_signature)) != sizeof(data_signature)) {
    return false;
  }

  return (ntohl(data_signature) == TLUT_SIGNATURE);
}

T_STATIC uint32_t prv_get_app_resource_id(ResAppNum res_app_num, TimelineResourceId timeline_id,
                                        TimelineResourceSize size) {
  // Load the entry from timeline resource lookup table
  uint32_t id;
  const uint32_t row = sizeof(TimelineLutEntry) * timeline_id;
  const uint32_t col = size * sizeof(id);
  const uint32_t offset = row + col + TLUT_DATA_OFFSET;
  const size_t bytes_read = sys_resource_load_range(res_app_num, TLUT_RESOURCE_ID, offset,
                                                    (uint8_t*)&id, sizeof(id));
  return (bytes_read == sizeof(id)) ? id : (uint32_t)RESOURCE_ID_INVALID;
}

static uint32_t prv_get_sys_resource_id(TimelineResourceId timeline_id, TimelineResourceSize size) {
  timeline_id &= ~SYSTEM_RESOURCE_FLAG;
  if (timeline_id >= NUM_TIMELINE_RESOURCES) {
    return RESOURCE_ID_INVALID;
  }
  return g_timeline_resources[timeline_id][size];
}

static bool prv_get_sys_resource(TimelineResourceId timeline_id, TimelineResourceSize size,
                                 AppResourceInfo *res_info) {
  const uint32_t res_id = prv_get_sys_resource_id(timeline_id, size);

  const bool success = (res_id != RESOURCE_ID_INVALID);
  if (success && res_info) {
    *res_info = (AppResourceInfo) {
      .res_app_num = SYSTEM_APP,
      .res_id = res_id,
    };
  }

  return success;
}

bool timeline_resources_is_system(TimelineResourceId timeline_id) {
  return (timeline_id & SYSTEM_RESOURCE_FLAG);
}

bool timeline_resources_get_id_system(TimelineResourceId timeline_id, TimelineResourceSize size,
                                      ResAppNum res_app_num, AppResourceInfo *res_info_out) {
  if (size >= TimelineResourceSizeCount) {
    return false;
  }

  if (timeline_resources_is_system(timeline_id)) {
    return prv_get_sys_resource(timeline_id, size, res_info_out);
  } else if (prv_validate_lut(res_app_num)) {
    const uint32_t res_id = prv_get_app_resource_id(res_app_num, timeline_id, size);
    if (res_id != RESOURCE_ID_INVALID) {
      const AppResourceInfo res_info = (AppResourceInfo) {
        .res_app_num = res_app_num,
        .res_id = res_id,
      };
      if (prv_is_app_published_resource_valid(&res_info)) {
        if (res_info_out) {
          *res_info_out = res_info;
        }
        return true;
      }
    }
  }

  return false;
}

void timeline_resources_get_id(const TimelineResourceInfo *timeline_res, TimelineResourceSize size,
                               AppResourceInfo *res_info_out) {
  PBL_ASSERTN(timeline_res && res_info_out && (size < TimelineResourceSizeCount));

  // Test the high bit to determine if system resource
  // TODO PBL-24499: Remove check for system uuid - this is only in place to ease the migration
  // from 3.4, where not all system pins use the correct id convention
  if (timeline_resources_is_system(timeline_res->res_id) || uuid_is_system(timeline_res->app_id)) {
    if (!prv_get_sys_resource(timeline_res->res_id, size, res_info_out)) {
      goto fail;
    }

    if (res_info_out->res_id == RESOURCE_ID_INVALID) {
      goto fail;
    }
  } else {
    const AppInstallId install_id = app_install_get_id_for_uuid(timeline_res->app_id);
    AppInstallEntry entry;
    if (!app_install_get_entry_for_install_id(install_id, &entry)) {
      goto fail;
    }

    // Only try to load icon if app was built with an SDK that supports it
    const Version first_pbw_icon_version = (Version) {
      .major = TIMELINE_RESOURCE_PBW_SUPPORT_FIRST_SDK_VERSION_MAJOR,
      .minor = TIMELINE_RESOURCE_PBW_SUPPORT_FIRST_SDK_VERSION_MINOR,
    };
    if (version_compare(entry.sdk_version, first_pbw_icon_version) < 0) {
      goto fail;
    }

    const ResAppNum res_app_num = app_install_get_app_icon_bank(&entry);
    if (!timeline_resources_get_id_system(timeline_res->res_id, size, res_app_num, res_info_out)) {
      char *uuid_str_buffer = task_zalloc_check(UUID_STRING_BUFFER_LENGTH);
      uuid_to_string(timeline_res->app_id, uuid_str_buffer);
      APP_LOG(APP_LOG_LEVEL_ERROR,
              "Failed to convert published id %d to resource id for app with uuid %s",
              timeline_res->res_id, uuid_str_buffer);
      task_free(uuid_str_buffer);
      goto fail;
    }
  }

  return;

fail:
  if (!prv_get_sys_resource(timeline_res->fallback_id, size, res_info_out)) {
    *res_info_out = (AppResourceInfo){0};
  }
}

DEFINE_SYSCALL(void, sys_timeline_resources_get_id, const TimelineResourceInfo *timeline_res,
               TimelineResourceSize size, AppResourceInfo *res_info) {
  if (PRIVILEGE_WAS_ELEVATED) {
    syscall_assert_userspace_buffer(timeline_res, sizeof(*timeline_res));
    syscall_assert_userspace_buffer(res_info, sizeof(*res_info));
  }
  if (!timeline_res || !res_info || (size >= TimelineResourceSizeCount)) {
    if (res_info) {
      *res_info = (AppResourceInfo) {0};
    }
    return;
  }
  timeline_resources_get_id(timeline_res, size, res_info);
}
