/* SPDX-FileCopyrightText: 2024 Google LLC */
/* SPDX-License-Identifier: Apache-2.0 */

#include "pfs.h"

#include <inttypes.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "console/prompt.h"
#include "drivers/flash.h"
#include "drivers/task_watchdog.h"
#include "flash_region/filesystem_regions.h"
#include "flash_region/flash_region.h"
#include "kernel/pbl_malloc.h"
#include "kernel/pebble_tasks.h"
#include "kernel/util/sleep.h"
#include "os/mutex.h"
#include "services/common/analytics/analytics.h"
#include "services/normal/filesystem/flash_translation.h"
#include "system/hexdump.h"
#include "system/logging.h"
#include "system/passert.h"
#include "util/attributes.h"
#include "util/crc8.h"
#include "util/legacy_checksum.h"
#include "util/math.h"

static PebbleRecursiveMutex *s_pfs_mutex = NULL;

#define IS_FILE_TYPE(file_type, type)   ((file_type) == (type))

#define PFS_PAGE_SIZE              FLASH_FILESYSTEM_BLOCK_SIZE
#define PFS_PAGES_PER_ERASE_SECTOR (SECTOR_SIZE_BYTES / PFS_PAGE_SIZE)
#define GC_REGION_SIZE             SECTOR_SIZE_BYTES

// The filesystem is broken into discrete blocks called 'pages'. Each page has
// a header that describes the contents contained within it. Static fields are
// CRC protected and are verified each time a file is opened. Convenience
// defines and the struct are defined below.

#define PAGE_FLAG_ERASED_PAGE            (1 << 0) // page erase completed
#define PAGE_FLAG_DELETED_PAGE           (1 << 1) // page was deleted
#define PAGE_FLAG_START_PAGE             (1 << 2) // first page of file
#define PAGE_FLAG_CONT_PAGE              (1 << 3) // continuation page of file

#define PAGE_FLAGS_BIT_SET(page_flags, type)  ((~(page_flags) & (type)) != 0)

#define DELETED_START_PAGE_MASK \
  (PAGE_FLAG_ERASED_PAGE | PAGE_FLAG_DELETED_PAGE | PAGE_FLAG_START_PAGE)
#define DELETED_CONT_PAGE_MASK \
  (PAGE_FLAG_ERASED_PAGE | PAGE_FLAG_DELETED_PAGE | PAGE_FLAG_CONT_PAGE)

// Header Layout Overview
//  First Page of a file:
//     | PageHeader | FileHeader | FileMetaData | name | File Data
//  Continuation Pages:
//     | PageHeader | File Data

#define IS_PAGE_TYPE(page_flags, type)                         \
  (PAGE_FLAGS_BIT_SET(page_flags, type)  &&                     \
   !PAGE_FLAGS_BIT_SET(page_flags, PAGE_FLAG_DELETED_PAGE))

#define SET_PAGE_FLAGS(page_flags, type) ((page_flags) &= ~(type))

#define PFS_MAGIC       0x50
#define PFS_VERS        0x01
#define PFS_CUR_VERSION ((PFS_MAGIC << 8) | PFS_VERS)

#define LAST_WRITTEN_TAG        0xfe
#define LAST_WRITTEN_UNMARK     0xfc

typedef struct PACKED PageHeader {
  uint16_t      version;
  uint8_t       last_written; //!< used by wear leveling algo
  uint8_t       page_flags;
  uint8_t       rsvd0[4];
  uint32_t      erase_count;
  uint8_t       rsvd1[9]; //!< for future extensions
  uint8_t       next_page_crc;
  uint16_t      next_page;
  uint32_t      hdr_crc; //!< a crc for all data that comes before it
} PageHeader;

typedef struct PACKED FileHeader {
  uint32_t      file_size;
  uint8_t       file_type;
  uint8_t       file_namelen;
  uint8_t       rsvd[6];
  uint32_t      hdr_crc;
} FileHeader;

// File metadata stored immediately after the header in the first page
typedef struct PACKED FileMetaData {
  uint16_t      tmp_state;
  uint16_t      create_state;
  uint16_t      delete_state;
  uint8_t       rsvd[10];
  uint8_t       uuid[16]; //!< rsvd for UUIDs in the future
  char          name[0];
} FileMetaData;

#define TMP_STATE_DONE          0x0
#define CREATE_STATE_DONE       0x0
#define DELETE_STATE_DONE       0x0

#define TMP_STATE_OFFSET        (offsetof(FileMetaData, tmp_state))
#define CREATE_STATE_OFFSET     (offsetof(FileMetaData, create_state))
#define DELETE_STATE_OFFSET     (offsetof(FileMetaData, delete_state))

#define AVAIL_BYTES_OFFSET      (sizeof(PageHeader))
#define FILEHEADER_OFFSET       (sizeof(PageHeader))
#define METADATA_OFFSET         (FILEHEADER_OFFSET + sizeof(FileHeader))
#define FILEDATA_LEN            (sizeof(FileHeader) + sizeof(FileMetaData))
#define FILE_NAME_OFFSET        (FILEHEADER_OFFSET + FILEDATA_LEN)

// defines & struct for data that needs to be tracked once a file is opened
#define INVALID_PAGE         ((uint16_t)~0)

#define GC_FILE_NAME "GC"
#define GC_DATA_VALID (0x1)

typedef struct {
  uint8_t version;
  uint8_t flags;
  uint16_t gc_start_page;
  uint32_t page_mask;
  uint8_t num_entries;
} GCData;

#define GCDATA_VALID(flags) ((~(flags) & GC_DATA_VALID) != 0)

typedef struct {
  uint16_t virtual_pg;
  uint16_t physical_pg;
  uint16_t contiguous_pgs;
} FilePageCache;

typedef struct File {
  // file specifics loaded from header
  char          *name;
  uint8_t       namelen;
  uint32_t      file_size;
  uint16_t      start_page; //!< the physical page at which the file begins
  uint16_t      start_offset; //!< offset at which file data begins
  uint8_t       file_type;

  // items dynamically changing
  uint8_t       op_flags;
  bool          is_tmp;
  uint32_t      offset; // the current offset within the file
  uint16_t      curr_page; // the current page the offset is on
  FilePageCache *pg_cache;
  uint8_t       pg_cache_len;
} File;

// The backing information tracked using the handle returned to callers
#define FD_STATUS_IN_USE        0x0 // A caller is using this fd
#define FD_STATUS_UNREFERENCED  0x1 // Valid data, no one using
#define FD_STATUS_FREE          0x2 // No data in the fd

// max number of files (cache size) that can be opened at any given time
#define PFS_FD_SET_SIZE         8
// 1 fd dedicated for GC (we always want an FD available for this operation!)
#define GC_FD_HANDLE_ID         (FD_INDEX_OFFSET + PFS_FD_SET_SIZE)
#define GC_FD_SET_SIZE          1
#define MAX_FD_HANDLES          (PFS_FD_SET_SIZE + GC_FD_SET_SIZE)
// Offset for FD numbers so that zero can't be a valid FD. This makes it much
// less likely for a file descriptor in an uninitialized object to reference a
// valid open file.
#define FD_INDEX_OFFSET         1001

static uint16_t time_closed_counter = 0;

typedef struct FileDesc {
  File          file;
  uint16_t      time_closed; //!< used for fd caching scheme
  uint8_t       fd_status;
} FileDesc;

static FileDesc s_pfs_avail_fd[MAX_FD_HANDLES];
// All accesses to s_pfs_avail_fd should be handled through the PFS_FD macro.
#define PFS_FD(fd) s_pfs_avail_fd[(fd)-FD_INDEX_OFFSET]

typedef struct GCBlock {
  bool     block_valid;
  uint8_t  block_writes;
  uint16_t gc_start_page;
} GCBlock;

static GCBlock s_gc_block;

// This is used by unit tests to clear out static state and simulate a reboot.
void pfs_reset_all_state(void) {
  s_gc_block = (GCBlock){};
  memset(s_pfs_avail_fd, 0, sizeof(s_pfs_avail_fd));
  time_closed_counter = 0;
}

#define FD_VALID(fd) ((((fd) >= FD_INDEX_OFFSET) && \
      ((fd) < (FD_INDEX_OFFSET+MAX_FD_HANDLES))) && \
      (PFS_FD(fd).fd_status == FD_STATUS_IN_USE))

#define VALID_TYPE(type) ((type) == FILE_TYPE_STATIC)

typedef struct {
  ListNode list_node;
  //! Name of the file to watch
  const char* name;
  //! Which events will invoke callbacks (see FILE_CHANGED_EVENT_ flags in pfs.h)
  uint8_t event_flags;
  //! Caller provided data pointer
  void *data;
  //! Callback pointer
  PFSFileChangedCallback callback;
} PFSFileChangedCallbackNode;

static uint8_t *s_pfs_page_flags_cache = NULL;
static uint16_t s_pfs_page_count = 0;
static uint32_t s_pfs_size = 0;
static ListNode *s_head_callback_node_list = NULL;

// In the interest of being able to leverage sector erases / minimize seek time
// for large files, deploying a variable length page size may be beneficial.
// Therefore, isolating the page offset related calculations to one location.
static uint32_t prv_page_to_flash_offset(uint16_t page) {
  return ((uint32_t)page * PFS_PAGE_SIZE);
}

static void prv_flash_read(void *buffer, uint32_t size, uint32_t offset) {
  if ((offset + size) <= s_pfs_size) {
    ftl_read(buffer, size, offset);
  } else {
    PBL_LOG(LOG_LEVEL_ERROR, "FS read out of bounds 0x%x", (int)offset);
  }
}

// Invalidates s_pfs_page_flags_cache for a given range of bytes. This should be called after the
// contents of the backing-flash are changed so that we re-read the page flags into our cache.
static void prv_invalidate_page_flags_cache(uint32_t offset, uint32_t size) {
  if (!s_pfs_page_flags_cache) {
    return;
  }

  // Prefetch any page flags which fall within the range of bytes which have been updated.
  const uint16_t start_page = offset / PFS_PAGE_SIZE;
  const uint16_t end_page = (offset + size - 1) / PFS_PAGE_SIZE;
  PBL_ASSERTN(end_page < s_pfs_page_count);
  const int page_flags_offset = offsetof(PageHeader, page_flags);
  for (uint16_t pg = start_page; pg <= end_page; pg++) {
    prv_flash_read(&s_pfs_page_flags_cache[pg], sizeof(s_pfs_page_flags_cache[pg]),
                   prv_page_to_flash_offset(pg) + page_flags_offset);
  }
}

static void prv_invalidate_page_flags_cache_all(void) {
  prv_invalidate_page_flags_cache(0, s_pfs_page_count * PFS_PAGE_SIZE);
}

static void prv_flash_write(const void *buffer, uint32_t size, uint32_t offset) {
  if ((offset + size) <= s_pfs_size) {
    ftl_write(buffer, size, offset);
    prv_invalidate_page_flags_cache(offset, size);
  } else {
    PBL_LOG(LOG_LEVEL_ERROR, "FS write out of bounds 0x%x", (int)offset);
  }
}

// Erases all pages for the sector which begins at 'start_page'
static void prv_flash_erase_sector(uint16_t start_page) {
  uint32_t offset = PFS_PAGE_SIZE * start_page;
  if (offset < s_pfs_size) {
    ftl_erase_sector(PFS_PAGE_SIZE * PFS_PAGES_PER_ERASE_SECTOR, offset);
    prv_invalidate_page_flags_cache(offset, PFS_PAGE_SIZE * PFS_PAGES_PER_ERASE_SECTOR);
  } else {
    PBL_LOG(LOG_LEVEL_ERROR, "Erase out of bounds, 0x%x", (int)start_page);
  }
}

static uint32_t free_bytes_in_page(uint16_t page) {
  return (PFS_PAGE_SIZE - AVAIL_BYTES_OFFSET);
}

static bool page_type_bits_set(uint8_t page_flags, uint8_t type_mask) {
  type_mask = ~type_mask;
  return (page_flags == type_mask);
}

static bool page_is_deleted(uint8_t page_flags) {
  return (page_type_bits_set(page_flags, DELETED_START_PAGE_MASK) ||
      page_type_bits_set(page_flags, DELETED_CONT_PAGE_MASK));
}

static bool page_is_erased(uint8_t page_flags) {
  return (page_type_bits_set(page_flags, PAGE_FLAG_ERASED_PAGE));
}

static bool page_is_unallocated(uint8_t page_flags) {
  return (page_is_deleted(page_flags) || page_is_erased(page_flags) ||
          (page_flags == 0xff));
}

static uint8_t prv_get_page_flags(uint16_t pg) {
#if UNITTEST
  // no caching for unit tests
  uint8_t flash_value = 0;
  prv_flash_read(&flash_value, sizeof(flash_value),
                 prv_page_to_flash_offset(pg) + offsetof(PageHeader, page_flags));
  return flash_value;
#else
  PBL_ASSERTN(s_pfs_page_flags_cache && (pg < s_pfs_page_count));
  return s_pfs_page_flags_cache[pg];
#endif
}

static void prv_build_page_flags_cache(void) {
#if UNITTEST
  // no caching for unit tests
  return;
#endif

  // if it already exists, free it first
  if (s_pfs_page_flags_cache) {
    kernel_free(s_pfs_page_flags_cache);
    s_pfs_page_flags_cache = NULL;
  }

  // if there are no pages in PFS, we don't need a cache
  if (s_pfs_page_count == 0) {
    return;
  }

  // allocate the new cache
  s_pfs_page_flags_cache = kernel_malloc_check(s_pfs_page_count * sizeof(*s_pfs_page_flags_cache));

  // read and set each of the page flags into the cache
  prv_invalidate_page_flags_cache_all();
}

static void update_curr_state(uint16_t start_page, uint32_t offset,
    uint16_t state) {
  offset += prv_page_to_flash_offset(start_page) + METADATA_OFFSET;
  prv_flash_write((uint8_t *)&state, sizeof(state), offset);
}

static bool get_curr_state(uint16_t start_page, uint32_t offset,
    uint16_t state) {
  uint16_t curr_state;
  offset += prv_page_to_flash_offset(start_page) + METADATA_OFFSET;
  prv_flash_read((uint8_t *)&curr_state, sizeof(state), offset);
  return (curr_state == state);
}

static bool is_create_complete(uint16_t start_page) {
  return (get_curr_state(start_page, CREATE_STATE_OFFSET, CREATE_STATE_DONE));
}

static bool is_delete_complete(uint16_t start_page) {
  return (get_curr_state(start_page, DELETE_STATE_OFFSET, DELETE_STATE_DONE));
}

static bool is_tmp_file(uint16_t start_page) {
  return (!get_curr_state(start_page, TMP_STATE_OFFSET, TMP_STATE_DONE));
}

static uint32_t compute_pg_header_crc(PageHeader *hdr) {
  PageHeader crc_hdr = *hdr;
  // don't factor fields which can change after file write into crc calc
  crc_hdr.last_written = 0xff;

  return legacy_defective_checksum_memory(&crc_hdr,
                                          offsetof(PageHeader, hdr_crc));
}

static uint32_t compute_file_header_crc(FileHeader *hdr) {
  return legacy_defective_checksum_memory(hdr, offsetof(FileHeader, hdr_crc));
}

// the start page is written to, the end page is not written to.
static void prv_write_erased_header_on_page_range(uint16_t start, uint16_t end,
    int erase_count) {
  // create a header representing an erase header
  PageHeader pg_hdr;
  memset(&pg_hdr, 0xff, sizeof(pg_hdr));
  pg_hdr.version = PFS_CUR_VERSION;
  pg_hdr.erase_count = erase_count;
  SET_PAGE_FLAGS(pg_hdr.page_flags, PAGE_FLAG_ERASED_PAGE);

  // write that header to each region in pfs
  uint8_t erased_header_size = offsetof(PageHeader, erase_count) + sizeof(pg_hdr.erase_count);
  for (uint16_t i = start; i < end; i++) {
    prv_flash_write((uint8_t*)&pg_hdr, erased_header_size, prv_page_to_flash_offset(i));
  }
}

typedef enum {
  PageHdrValid = 0,
  PageAndFileHdrValid = 1,
  HdrCrcCorrupt = -1,
  HdrVersionCheckFail = -2
} ReadHeaderStatus;

static ReadHeaderStatus read_header(uint16_t page, PageHeader *pg_hdr,
    FileHeader *file_hdr) {
  prv_flash_read((uint8_t *)pg_hdr, sizeof(*pg_hdr), prv_page_to_flash_offset(page));

  if (compute_pg_header_crc(pg_hdr) != pg_hdr->hdr_crc) {
    return (HdrCrcCorrupt);
  }

  if (pg_hdr->version > PFS_CUR_VERSION) {
    PBL_LOG(LOG_LEVEL_ERROR, "Unexpected Version Header, 0x%x",
        (int)pg_hdr->version);
    return (HdrVersionCheckFail); // let caller handle
  }

  if (!IS_PAGE_TYPE(pg_hdr->page_flags, PAGE_FLAG_START_PAGE)) {
    return (PageHdrValid);
  }

  prv_flash_read((uint8_t *)file_hdr, sizeof(*file_hdr), FILEHEADER_OFFSET +
      prv_page_to_flash_offset(page));

  if (compute_file_header_crc(file_hdr) != file_hdr->hdr_crc) {
    return (HdrCrcCorrupt);
  }

  return (PageAndFileHdrValid);
}

static status_t write_file_header(FileHeader *hdr, uint16_t pg) {
  hdr->hdr_crc = compute_file_header_crc(hdr);
  prv_flash_write((uint8_t *)hdr, sizeof(*hdr), prv_page_to_flash_offset(pg) +
      FILEHEADER_OFFSET);
  return (S_SUCCESS);
}

static status_t write_pg_header(PageHeader *hdr, uint16_t pg) {
  // recover current erase count which is updated in erase routine
  prv_flash_read((uint8_t *)&hdr->erase_count, sizeof(hdr->erase_count),
      prv_page_to_flash_offset(pg) + offsetof(PageHeader, erase_count));
  prv_flash_read((uint8_t *)&hdr->last_written, sizeof(hdr->last_written),
     prv_page_to_flash_offset(pg) + offsetof(PageHeader, last_written));

  hdr->hdr_crc = compute_pg_header_crc(hdr);
  prv_flash_write((uint8_t *)hdr, sizeof(*hdr), prv_page_to_flash_offset(pg));

  return (S_SUCCESS);
}

// note: the goal here is to do as few flash reads as possible
// while scanning the flash to find a given file.
static status_t locate_flash_file(const char *name, uint16_t *page) {
  const int file_namelen_offset = FILEHEADER_OFFSET +
      offsetof(FileHeader, file_namelen);
  uint8_t namelen = strlen(name);

  for (uint16_t pg = 0; pg < s_pfs_page_count; pg++) {
    PageHeader pg_hdr;
    FileHeader file_hdr;
    pg_hdr.page_flags = prv_get_page_flags(pg);

    if (!IS_PAGE_TYPE(pg_hdr.page_flags, PAGE_FLAG_START_PAGE)) {
      continue; // only start pages contain file name info
    }

    prv_flash_read((uint8_t *)&file_hdr.file_namelen, sizeof(file_hdr.file_namelen),
        prv_page_to_flash_offset(pg) + file_namelen_offset);

    if (file_hdr.file_namelen == namelen) {
      char file_name[namelen];

      prv_flash_read((uint8_t *)file_name, namelen, prv_page_to_flash_offset(pg) +
          FILE_NAME_OFFSET);

      if ((memcmp(name, file_name, namelen) == 0) && (!is_tmp_file(pg))) {

        if (read_header(pg, &pg_hdr, &file_hdr) == HdrCrcCorrupt) {
          PBL_LOG(LOG_LEVEL_WARNING, "%d: CRC corrupt", pg);
          continue;
        }

        *page = pg;
        return (S_SUCCESS);
      }
    }
  }

  return (E_DOES_NOT_EXIST);
}

// Populates 'hdr' with what the new erase header for the 'page' specified
// should look like
static int get_updated_erase_hdr(PageHeader *hdr, uint16_t page) {
  memset(hdr, 0xff, sizeof(*hdr));

  // before wiping a page, get its erase_count. This is not currently used but
  // enables future wear leveling improvements / analysis
  prv_flash_read((uint8_t *)&hdr->erase_count, sizeof(hdr->erase_count),
      prv_page_to_flash_offset(page) + offsetof(PageHeader, erase_count));
  prv_flash_read((uint8_t *)&hdr->last_written, sizeof(hdr->last_written),
      prv_page_to_flash_offset(page) + offsetof(PageHeader, last_written));

  // feed watchdog since erases can take a while & give lower priority tasks
  // a little time in case we are calling this from a high priority task and
  // stalling them
  task_watchdog_bit_set(pebble_task_get_current());
  psleep(1);

  // mark the page as erased. This way we know that the erase completed
  // next time we scan the sector
  SET_PAGE_FLAGS(hdr->page_flags, PAGE_FLAG_ERASED_PAGE);
  if (hdr->erase_count == 0xffffffff) {
    // should only happen after a filesystem format so assume 0 but could
    // also occur if we reboot during an erase cycle
    hdr->erase_count = 0;
  }
  hdr->erase_count++;
  hdr->version = PFS_CUR_VERSION;

  if (hdr->last_written != LAST_WRITTEN_TAG) {
    hdr->last_written = 0xff; // reset last written tag
  }

  return (S_SUCCESS);
}

static int s_last_page_written = 0;
#if UNITTEST
static int s_test_last_page_written_override = -1;
#endif

static void update_last_written_page(void) {
  for (uint16_t pg = 0; pg < s_pfs_page_count; pg++) {
    PageHeader hdr;
    prv_flash_read((uint8_t *)&hdr.last_written, sizeof(hdr.last_written),
        prv_page_to_flash_offset(pg) + offsetof(PageHeader, last_written));
    if (hdr.last_written == LAST_WRITTEN_TAG) {
      s_last_page_written = pg;
      PBL_LOG(LOG_LEVEL_INFO, "Last written page %d", (int)s_last_page_written);
      return;
    }
  }

  // should only happen after a filesystem format
  PBL_LOG(LOG_LEVEL_WARNING, "Couldn't resolve last written pg");
  s_last_page_written = s_pfs_page_count - 1;
#if UNITTEST
  if (s_test_last_page_written_override != -1) {
    s_last_page_written = s_test_last_page_written_override;
  }
#endif
}

//! @param region - The erase sector we want to scan through
//! @param first_free_page - Populated with the first erased page found
//!     in the region. If none are found, populated with INVALID_PAGE
//! @return A bitmask indicating which pages in the sector are occupied.
//!     For example, 0b1001 would indicate page 0 and page 3 within the sector
//!     are in use
static uint32_t prv_get_sector_page_status(uint16_t region,
     uint16_t *first_free_page) {

  // our bitmask needs to be large enough to describe all the pages in a sector
  _Static_assert((sizeof(uint32_t) * 8) >= PFS_PAGES_PER_ERASE_SECTOR,
      "Number of PFS pages is larger than bitmask");

  *first_free_page = INVALID_PAGE;

  uint16_t start_pg = region * PFS_PAGES_PER_ERASE_SECTOR;
  uint16_t end_pg = start_pg + PFS_PAGES_PER_ERASE_SECTOR;
  uint32_t sectors_active = 0;
  for (uint16_t pg = start_pg; pg < end_pg; pg++) {
    uint8_t page_flags = prv_get_page_flags(pg);

    if (page_is_erased(page_flags)) {
      if (*first_free_page == INVALID_PAGE) {
        *first_free_page = pg;
      }
    } else if (!page_is_unallocated(page_flags)) {
      sectors_active |= (0x1 << (pg % PFS_PAGES_PER_ERASE_SECTOR));
    }
  }

  return (sectors_active);
}

//! Scans through the filesystem and finds a sector with no pages that
//! are active
//!
//! @param skip_gc_region - will skip checking the region that is
//!     used for garbage collection
//! @return the beginning page in the region which is free or -1 on failure
static int prv_find_free_erase_region(bool skip_gc_region) {
  int num_erase_regions = s_pfs_page_count / PFS_PAGES_PER_ERASE_SECTOR;
  int start_region = s_last_page_written / PFS_PAGES_PER_ERASE_SECTOR;
  int end_region = start_region + num_erase_regions;

  uint16_t gc_erase_block = s_gc_block.gc_start_page / PFS_PAGES_PER_ERASE_SECTOR;

  for (int region = start_region; region < end_region; region++) {
    int erase_region = region % num_erase_regions;

    if (skip_gc_region && (erase_region == gc_erase_block)) {
      continue;
    }

    uint16_t free_pg;
    uint32_t sectors_active = prv_get_sector_page_status(erase_region, &free_pg);
    if ((__builtin_popcount(sectors_active) == 0)) {
      return (erase_region * PFS_PAGES_PER_ERASE_SECTOR);
    }
  }

  return (-1);
}

static status_t garbage_collect_sector(uint16_t *free_page,
    uint16_t sector_start_page, uint32_t sectors_active);

//! Updates the last written page to point to next_page
static NOINLINE void prv_update_last_written_page(uint16_t next_page) {
  PageHeader hdr = { 0 };

  uint16_t prev_written_page = s_last_page_written;
  // unmark the previous page as last written (should only have one pg
  // marked as written at any given time).
  prv_flash_read((uint8_t *)&hdr.last_written, sizeof(hdr.last_written),
      prv_page_to_flash_offset(prev_written_page) +
      offsetof(PageHeader, last_written));

  if (hdr.last_written == LAST_WRITTEN_TAG) {
    hdr.last_written = LAST_WRITTEN_UNMARK;
    prv_flash_write((uint8_t *)&hdr.last_written, sizeof(hdr.last_written),
        prv_page_to_flash_offset(prev_written_page) +
        offsetof(PageHeader, last_written));
  }

  hdr.last_written = LAST_WRITTEN_TAG;

  prv_flash_write((uint8_t *)&hdr.last_written, sizeof(hdr.last_written),
      prv_page_to_flash_offset(next_page) + offsetof(PageHeader, last_written));
}

//! The wear leveling strategy deployed is as follows:
//!    Always track the last page which was written. Every time a new page needs
//!    to be allocated, search for the next page that comes after the
//!    'last written' page.
//!
//! Note:
//!  - This is the only routine that should ever tag a page as last written.
//!  - This routine can be called at any time to force garbage collection at
//!    opportune times (i.e in an idle task). For this scenario, use_page
//!    described below should be 'false'.
//! @param free_page - Populated with a free page that is erased and available
//!     to be written on. Value should initially be set to INVALID_PAGE if it's
//!     the first page being allocated for a file. Afterward the value should be
//!     the previously allocated page
//! @param use_gc_allocator - should be true iff the page should be allocated
//!      from the region dedicated for garbage collection handling
//! @param use_page - should be true iff the page is about to be used in a file.
static status_t find_free_page(uint16_t *free_page, bool use_gc_allocator,
    bool use_page) {

  // if we are allocating a file from the garbage collection region,
  // we don't need to search for free pages since we know what ones to use
  if (use_gc_allocator) {
    uint16_t next_page = (*free_page == INVALID_PAGE) ? s_gc_block.gc_start_page :
        (*free_page + 1);
    PBL_ASSERTN(s_gc_block.block_valid && (next_page >= s_gc_block.gc_start_page) &&
        (next_page < (s_gc_block.gc_start_page + PFS_PAGES_PER_ERASE_SECTOR)));
    *free_page = next_page;
    return (S_SUCCESS);
  }

  uint16_t next_page = INVALID_PAGE;
  uint16_t start_pg = (s_last_page_written + 1) % s_pfs_page_count;
  uint16_t remaining_pgs_in_block = PFS_PAGES_PER_ERASE_SECTOR -
      (start_pg % PFS_PAGES_PER_ERASE_SECTOR);

  uint16_t gc_erase_region = s_gc_block.gc_start_page / PFS_PAGES_PER_ERASE_SECTOR;
  bool in_gc_region = (gc_erase_region == (start_pg / PFS_PAGES_PER_ERASE_SECTOR));

  // are we looking for free pages in the sector we last wrote to?
  if (remaining_pgs_in_block < PFS_PAGES_PER_ERASE_SECTOR) {
    // are any of the pages already erased?
    for (uint16_t pg = 0; pg < remaining_pgs_in_block && !in_gc_region; pg++) {
      uint16_t curr_page = start_pg + pg;
      uint8_t page_flags = prv_get_page_flags(curr_page);

      if (page_is_erased(page_flags)) {
        next_page = curr_page;
        break;
      }
    }

    start_pg += remaining_pgs_in_block;
  }

  // we should now be processing on a sector aligned boundary
  PBL_ASSERTN((start_pg % PFS_PAGES_PER_ERASE_SECTOR) == 0);

  // if we could not find a free page in the sector we were previosuly using
  // we need to scan through the erase regions and either perform some garbage
  // collection or find an erased page in another erase region
  if (next_page == INVALID_PAGE) {
    int num_erase_regions = s_pfs_page_count / PFS_PAGES_PER_ERASE_SECTOR;
    uint16_t start_region = start_pg / PFS_PAGES_PER_ERASE_SECTOR;

    for (uint16_t region = 0; region < num_erase_regions; region++) {
      uint16_t curr_region = (region + start_region) % num_erase_regions;

      if (s_gc_block.block_valid && (gc_erase_region == curr_region)) {
        // don't use pre-allocated garbage collection regions
        continue;
      }

      uint32_t sectors_active = prv_get_sector_page_status(curr_region, &next_page);
      if (next_page != INVALID_PAGE) {
        // we have found a page which is already erased
        break;
      } else if (__builtin_popcount(sectors_active) < PFS_PAGES_PER_ERASE_SECTOR) {
        // we can erase this region and have at least 1 free page after
        uint16_t sector_start_pg = curr_region * PFS_PAGES_PER_ERASE_SECTOR;
        garbage_collect_sector(&next_page, sector_start_pg, sectors_active);
        break;
      }
    }
  }

  if (next_page != INVALID_PAGE) { // a free page was found
    if (use_page) {
      prv_update_last_written_page(next_page);
    }

    *free_page = s_last_page_written = next_page;
    return (S_SUCCESS);
  }

  return (E_OUT_OF_STORAGE);
}

//! Note: expects that the caller does _not_ hold the pfs mutex
//! Note: If pages are already pre-erased on the FS, this routine will return
//!  very quickly. If we need to do erases, it will take longer becauses this
//!  operation can take seconds to complete on certain flash parts
//!
//! @param file_size - The amount of file space to erase
//! @param max_elapsed_ticks - The max amount of time to spend attempting to
//!    find / create the free space. If 0, then there is no timeout
static void pfs_prepare_for_file_creation(uint32_t file_size,
                                          uint32_t max_elapsed_ticks) {
  uint16_t pages_to_find = (file_size + PFS_PAGE_SIZE) / PFS_PAGE_SIZE;
  uint16_t free_page = 0;

  uint32_t start_ticks = rtc_get_ticks();

  uint16_t last_written_page = s_last_page_written;
  while ((pages_to_find > 0) && (free_page != INVALID_PAGE)) {
    mutex_lock_recursive(s_pfs_mutex);
    find_free_page(&free_page, false, false);
    mutex_unlock_recursive(s_pfs_mutex);
    // TODO: might be nice to only sleep here if we had to perform GC as part
    // of finding a free page
    if ((pages_to_find % 4) == 0) {
      psleep(2);
    }
    pages_to_find--;

    uint32_t elapsed_ticks = rtc_get_ticks() - start_ticks;
    if (max_elapsed_ticks != 0 && (elapsed_ticks > max_elapsed_ticks)) {
      break;
    }
  }

  mutex_lock_recursive(s_pfs_mutex);
  s_last_page_written = last_written_page; // reset our tracker
  mutex_unlock_recursive(s_pfs_mutex);
}

// In the future, the next_page field may be updated dynamically (i.e to resize
// a file). Use a CRC to catch corruption issues in this field.
static uint8_t crc8_next_page(uint16_t next_page) {
  return crc8_calculate_bytes((uint8_t*)&next_page, sizeof(next_page), true /* big_endian */);
}

static status_t get_next_page(uint16_t curr_page, uint16_t *next_page) {
  PageHeader hdr;
  prv_flash_read((uint8_t *)&hdr.next_page_crc, sizeof(hdr.next_page_crc) +
      sizeof(hdr.next_page), prv_page_to_flash_offset(curr_page) +
      offsetof(PageHeader, next_page_crc));
  *next_page = hdr.next_page;

  if (*next_page == INVALID_PAGE) {
    return (S_NO_MORE_ITEMS);
  }

  if (crc8_next_page(*next_page) == hdr.next_page_crc) {
    if (*next_page < s_pfs_page_count) {
      return (S_SUCCESS);
    }
  }

  return (E_INTERNAL); // the next page pointer is corrupt
}

static status_t unlink_flash_file(uint16_t page) {
  uint16_t first_page = page;
  if (page > s_pfs_page_count) { // should never happen
    return (E_INTERNAL);
  }

  // Mark the files to indicate that they are ready to be erased
  PageHeader hdr;
  hdr.page_flags = 0xff;
  SET_PAGE_FLAGS(hdr.page_flags, PAGE_FLAG_DELETED_PAGE);
  int rv = S_SUCCESS;
  int unlink_count = 0;
  do {
    if ((page > s_pfs_page_count) || (unlink_count > s_pfs_page_count)) {
      rv = E_INTERNAL; // should never happen
      break;
    }
    prv_flash_write((uint8_t *)&hdr.page_flags, sizeof(hdr.page_flags),
        prv_page_to_flash_offset(page) + offsetof(PageHeader, page_flags));

    unlink_count++;
  } while (get_next_page(page, &page) == S_SUCCESS);

  // Add a tag to indicate that all pages within a file have been marked for
  // deletion we check for this during reboot to clean up a partial delete
  update_curr_state(first_page, DELETE_STATE_OFFSET, DELETE_STATE_DONE);

  return (rv);
}

static status_t create_flash_file(File *f) {
  status_t rv;
  uint16_t start_page = INVALID_PAGE;

  PageHeader pg_hdr;
  memset(&pg_hdr, 0xff, sizeof(pg_hdr));

  bool use_gc_allocator = (strcmp(f->name, GC_FILE_NAME) == 0);

  if ((rv = find_free_page(&start_page, use_gc_allocator, true)) != S_SUCCESS) {
    return (rv);
  }

  pg_hdr.version = PFS_CUR_VERSION;
  SET_PAGE_FLAGS(pg_hdr.page_flags,
      PAGE_FLAG_START_PAGE | PAGE_FLAG_ERASED_PAGE);

  // Note: We have already allocated 1 pg so just subtract 1 to roundup
  // We assume all pages are the same size
  int pgs_needed = (f->file_size + FILEDATA_LEN + strlen(f->name) - 1) /
      free_bytes_in_page(start_page);
  uint16_t curr_page = start_page;
  uint16_t next_page = start_page;

  for (; pgs_needed >= 0; pgs_needed--) {
    // flag the page as in use
    prv_flash_write((uint8_t *)&pg_hdr.page_flags, sizeof(pg_hdr.page_flags),
        prv_page_to_flash_offset(curr_page) + offsetof(PageHeader, page_flags));

    if (pgs_needed > 0) { // do we need to find a free page
      if ((rv = find_free_page(&next_page, use_gc_allocator, true)) != S_SUCCESS) {
        unlink_flash_file(start_page); // on failure, unallocate
        return (rv);
      }
      pg_hdr.next_page_crc = crc8_next_page(next_page);
      pg_hdr.next_page = next_page;
      write_pg_header(&pg_hdr, curr_page);
      curr_page = next_page;

      // continuation page header settings
      memset(&pg_hdr, 0xff, sizeof(PageHeader));
      pg_hdr.version = PFS_CUR_VERSION;
      SET_PAGE_FLAGS(pg_hdr.page_flags,
          PAGE_FLAG_CONT_PAGE | PAGE_FLAG_ERASED_PAGE);
    } else {
      write_pg_header(&pg_hdr, curr_page);
      break; // we are done
    }
  }

  // we have succesfully allocated space for the file, so add file specific info
  f->start_page = f->curr_page = start_page;

  FileHeader file_hdr;
  memset(&file_hdr, 0xff, sizeof(file_hdr));
  file_hdr.file_namelen = strlen(f->name);
  file_hdr.file_size = f->file_size;
  file_hdr.file_type = f->file_type;
  write_file_header(&file_hdr, start_page);

  prv_flash_write((uint8_t *)f->name, strlen(f->name),
      prv_page_to_flash_offset(start_page) + FILE_NAME_OFFSET);

  if (!f->is_tmp) {
    update_curr_state(f->start_page, TMP_STATE_OFFSET, TMP_STATE_DONE);
  }

  // finally, mark the creation as complete
  update_curr_state(start_page, CREATE_STATE_OFFSET, CREATE_STATE_DONE);

  return (S_SUCCESS);
}

static status_t scan_to_offset(File *f, uint32_t *pg_offset) {
  uint32_t data_offset = f->offset + f->start_offset;

  // a read or write could have ended at a page boundary so check for that
  if ((f->curr_page == INVALID_PAGE) ||
      ((data_offset % free_bytes_in_page(f->curr_page)) == 0)) {
    uint16_t next_page = f->start_page;
    int pages_to_seek = (data_offset / free_bytes_in_page(f->start_page));

    int closest_match = -1;
    if (((f->op_flags & OP_FLAG_USE_PAGE_CACHE) != 0) && (f->pg_cache != NULL)) {

      // Flash pages are singly linked together with the next pointer located
      // on the current flash page. This means the optimal page to find in the
      // cache is the one closest to what we are looking for without going past
      // it
      for (int i = 0; i < f->pg_cache_len; i++) {
        FilePageCache *pgc = &f->pg_cache[i];
        if (pgc->virtual_pg > pages_to_seek) {
          continue;
        } else if ((closest_match == -1) ||
            (f->pg_cache[closest_match].virtual_pg < pgc->virtual_pg)) {
          closest_match = i;
        }
      }
    }

    if (closest_match != -1) {
      FilePageCache *close_pg = &f->pg_cache[closest_match];

      pages_to_seek -= f->pg_cache[closest_match].virtual_pg;
      next_page = f->pg_cache[closest_match].physical_pg;

      // if we still are not on the page we are looking for, see how
      // many contiguous pages we can skip ahead.
      if (pages_to_seek > 0) {
        uint16_t contig_pgs = MIN(close_pg->contiguous_pgs, pages_to_seek);
        pages_to_seek -= contig_pgs;
        next_page += contig_pgs;
      }
    }

    for (uint16_t i = 0; i < pages_to_seek; i++) {
      if (get_next_page(next_page, &next_page) != S_SUCCESS) {
        return (E_RANGE);
      }
    }
    f->curr_page = next_page;
  }

  *pg_offset = data_offset % free_bytes_in_page(f->curr_page);
  return (S_SUCCESS);
}

static int mark_fd_free(int fd) {
  if (PFS_FD(fd).file.name != NULL) {
    kernel_free(PFS_FD(fd).file.name);
    PFS_FD(fd).file.name = NULL;
  }
  if (PFS_FD(fd).file.pg_cache != NULL) {
    kernel_free(PFS_FD(fd).file.pg_cache);
    PFS_FD(fd).file.pg_cache = NULL;
    PFS_FD(fd).file.pg_cache_len = 0;
  }

  PFS_FD(fd).fd_status = FD_STATUS_FREE;

  return (S_SUCCESS);
}

typedef enum {
  FDBusy = 2,
  FDAlreadyLoaded = 1,
  FDAvail = 0,
  NoFDAvail = -1
} AvailFdStatus;

//! @param is_tmp specified to indicate whether or not you are looking for
//!        a tmp file
static AvailFdStatus get_avail_fd(const char *name, int *fdp, bool is_tmp) {
  // First search to see if the fd has already been located
  for (int fd = FD_INDEX_OFFSET; fd < FD_INDEX_OFFSET+MAX_FD_HANDLES; fd++) {
    File *f = &PFS_FD(fd).file;
    if ((f->is_tmp == is_tmp) && (f->name != NULL)) {
      if (strcmp(f->name, name) == 0) {
        PBL_ASSERTN(PFS_FD(fd).fd_status != FD_STATUS_FREE);
        *fdp = fd;
        return ((PFS_FD(fd).fd_status == FD_STATUS_IN_USE) ? FDBusy : FDAlreadyLoaded);
      }
    }
  }

  // a simple least-recently-accessed cache scheme
  int unref = -1;
  uint16_t curr_time_closed = 0;

  for (int fd = FD_INDEX_OFFSET; fd < FD_INDEX_OFFSET+PFS_FD_SET_SIZE; fd++) {
    if (PFS_FD(fd).fd_status == FD_STATUS_FREE) {
      *fdp = fd;
      return (FDAvail);
    }
    if (PFS_FD(fd).fd_status == FD_STATUS_UNREFERENCED) {
      if ((unref == -1) || (PFS_FD(fd).time_closed < curr_time_closed)) {
        unref = fd;
        curr_time_closed = PFS_FD(fd).time_closed;
      }
    }
  }

  *fdp = unref;
  if (unref != -1) {
    mark_fd_free(unref); // clean up previous file state
  }

  return ((*fdp != -1) ? FDAvail : NoFDAvail);
}

/*
 * Exported PFS APIs
 */

size_t pfs_get_file_size(int fd) {
  mutex_lock_recursive(s_pfs_mutex);

  size_t res = 0;
  if (FD_VALID(fd)) {
    res = PFS_FD(fd).file.file_size;
  }

  mutex_unlock_recursive(s_pfs_mutex);
  return (res);
}

int pfs_read(int fd, void *buf_ptr, size_t size) {
  uint8_t *buf = buf_ptr;
  mutex_lock_recursive(s_pfs_mutex);

  int res = E_UNKNOWN;
  if (!FD_VALID(fd) || (buf == NULL) || (size == 0)) {
    res = E_INVALID_ARGUMENT;
    goto cleanup;
  }

  File *file = &PFS_FD(fd).file;

  if ((file->op_flags & OP_FLAG_READ) == 0) {
    res = E_INVALID_ARGUMENT;
    goto cleanup;
  }

  if ((file->offset + size) > file->file_size) {
    PBL_LOG(LOG_LEVEL_DEBUG, "Out of bound read at %d",
        (int)(file->offset + size));
    res = E_RANGE;
    goto cleanup;
  }

  uint32_t pg_offset;
  if (scan_to_offset(file, &pg_offset) != S_SUCCESS) {
    res = E_INTERNAL;
    goto cleanup;
  }

  // we have found the page from which to start reading data from
  size_t bytes_read = 0;
  while (bytes_read < size) {
    size_t bytes_to_read = MIN(free_bytes_in_page(file->curr_page) - pg_offset,
        size - bytes_read);

    prv_flash_read(buf + bytes_read, bytes_to_read,
        prv_page_to_flash_offset(file->curr_page) + AVAIL_BYTES_OFFSET + pg_offset);

    bytes_read += bytes_to_read;
    file->offset += bytes_to_read;

    if (bytes_read == size) {
      break; // we are done
    }

    pg_offset = 0; // first usable byte next page
    if (get_next_page(file->curr_page, &file->curr_page) != S_SUCCESS) {
      PBL_LOG(LOG_LEVEL_WARNING, "R:Couldn't find next page for %d",
          file->curr_page);
      res = E_INTERNAL;
      goto cleanup;
    }
  }

  res = bytes_read;
cleanup:
  mutex_unlock_recursive(s_pfs_mutex);
  return (res);
}

int pfs_seek(int fd, int offset, FSeekType seek_type) {
  mutex_lock_recursive(s_pfs_mutex);
  int res = E_UNKNOWN;
  if (!FD_VALID(fd)) {
    res = E_INVALID_ARGUMENT;
    goto cleanup;
  }

  int new_offset = PFS_FD(fd).file.offset;
  if (seek_type == FSeekSet) {
    new_offset = offset;
  } else if (seek_type == FSeekCur) {
    new_offset += offset;
  }

  // allow one to seek to very EOF
  if ((new_offset >= 0) &&
      (new_offset <= (int)PFS_FD(fd).file.file_size)) {

    if (PFS_FD(fd).file.offset != (uint32_t)new_offset) {
      PFS_FD(fd).file.offset = (uint32_t)new_offset;
      PFS_FD(fd).file.curr_page = INVALID_PAGE;
    }
    res = new_offset;
  } else {
    res = E_RANGE;
  }

cleanup:
  mutex_unlock_recursive(s_pfs_mutex);
  return (res);
}

int pfs_write(int fd, const void *buf_ptr, size_t size) {
  const uint8_t *buf = buf_ptr;
  mutex_lock_recursive(s_pfs_mutex);
  int res = E_UNKNOWN;
  if (!FD_VALID(fd) || (buf == NULL) || (size == 0)) {
    res = E_INVALID_ARGUMENT;
    goto cleanup;
  }

  File *file = &PFS_FD(fd).file;

  if ((file->op_flags & (OP_FLAG_WRITE | OP_FLAG_OVERWRITE)) == 0) {
    res = E_INVALID_ARGUMENT;
    goto cleanup;
  }

  if ((file->offset + size) > file->file_size) {
    res = E_RANGE;
    goto cleanup;
  }

  uint32_t pg_offset;
  if (scan_to_offset(file, &pg_offset) != S_SUCCESS) {
    res = E_INTERNAL;
    goto cleanup;
  }

  size_t bytes_written = 0;
  while (bytes_written < size) {
    size_t bytes_to_write = MIN(free_bytes_in_page(file->curr_page) - pg_offset,
        size - bytes_written);

    prv_flash_write(buf + bytes_written, bytes_to_write,
        prv_page_to_flash_offset(file->curr_page) + AVAIL_BYTES_OFFSET + pg_offset);

    bytes_written += bytes_to_write;
    file->offset += bytes_to_write;

    if (bytes_written == size) {
      break;
    }

    pg_offset = 0; // first usable byte next page
    if (get_next_page(file->curr_page, &file->curr_page) != S_SUCCESS) {
      PBL_LOG(LOG_LEVEL_WARNING, "W:Couldn't find next page for %d",
          file->curr_page);
      res = E_INTERNAL;
      goto cleanup;
    }
  }

  res = bytes_written;
cleanup:
  mutex_unlock_recursive(s_pfs_mutex);
  return (res);
}

uint32_t pfs_get_size(void) {
  // one sector is needed for internal book keeping
  return s_pfs_size - GC_REGION_SIZE;
}

void pfs_set_size(uint32_t new_size, bool new_region_erased) {
  uint32_t prev_size = s_pfs_size;
  s_pfs_size = new_size;
  s_pfs_page_count = new_size / PFS_PAGE_SIZE;

  // re-build the flags cache
  prv_build_page_flags_cache();

  if (new_region_erased) {
    prv_write_erased_header_on_page_range((prev_size/PFS_PAGE_SIZE),
        (new_size/PFS_PAGE_SIZE), 1);
  }

  update_last_written_page();
}

bool pfs_active_in_region(uint32_t start_address, uint32_t ending_address) {
  uint16_t starting_page = start_address / PFS_PAGE_SIZE;
  uint16_t ending_page = (ending_address) / PFS_PAGE_SIZE;

  for (uint16_t pg = starting_page; pg < ending_page; pg++) {

    PageHeader hdr;

    // read version first, check magic, then check version and make sure it makes sense
    prv_flash_read((uint8_t *)&hdr.version, sizeof(hdr.version),
        prv_page_to_flash_offset(pg) + offsetof(PageHeader, version));

    if ((hdr.version >> 8) != PFS_MAGIC) {
      continue;
    }

    if (hdr.version > PFS_CUR_VERSION) {
      PBL_LOG(LOG_LEVEL_WARNING, "Incompatible version of PFS active, 0x%x",
          hdr.version);

      // pfs filesystem is a newer version than we support
      return (false);
    }

    hdr.page_flags = prv_get_page_flags(pg);
    // read the header flags to see if the page is a file start page or an erased page
    if (IS_PAGE_TYPE(hdr.page_flags, PAGE_FLAG_ERASED_PAGE)
       || IS_PAGE_TYPE(hdr.page_flags, PAGE_FLAG_START_PAGE)
       || IS_PAGE_TYPE(hdr.page_flags, PAGE_FLAG_CONT_PAGE)
       || page_is_deleted(hdr.page_flags)) {

      return (true);
    }
  }

  // pfs filesystem is not active
  return (false);
}

// migration utility
// Returns true if valid PFS file found, false otherwise
bool pfs_active(void) {
  return pfs_active_in_region(0, s_pfs_size);
}

// Scans through the filesystem to see if we rebooted while a file was in the
// middle of being created and cleans up these partial files.
void pfs_reboot_cleanup(void) {
  static uint16_t curr_pg = 0;

  for (; curr_pg < s_pfs_page_count; curr_pg++) {
    uint8_t page_flags = prv_get_page_flags(curr_pg);

    if (IS_PAGE_TYPE(page_flags, PAGE_FLAG_START_PAGE)) {
      if (!is_create_complete(curr_pg)) { // make sure file creation completed
        PBL_LOG(LOG_LEVEL_WARNING, "File at %d creation did not complete ",
            curr_pg);
        unlink_flash_file(curr_pg);
      } else if (is_tmp_file(curr_pg)) { // make sure this isn't a temp file
        PBL_LOG(LOG_LEVEL_WARNING, "Removing temp file at %d", curr_pg);
        unlink_flash_file(curr_pg);
      }
    } else if (page_type_bits_set(page_flags, DELETED_START_PAGE_MASK) &&
        !is_delete_complete(curr_pg)) {
      PBL_LOG(LOG_LEVEL_WARNING, "Delete of %d did not complete", curr_pg);
      unlink_flash_file(curr_pg);
    }
  }

  update_last_written_page();
}

static void prv_handle_sector_erase(uint16_t start_page, bool update_erase_count) {
  if (!update_erase_count) {
    prv_flash_erase_sector(start_page);
    return;
  }

  uint16_t max_erase = 0;
  uint16_t last_written_pg = INVALID_PAGE;
  PageHeader hdr;
  for (int i = 0; i < PFS_PAGES_PER_ERASE_SECTOR; i++) {
    get_updated_erase_hdr(&hdr, i + start_page);
    if (hdr.erase_count > max_erase) {
      max_erase = hdr.erase_count;
    }

    if (hdr.last_written == LAST_WRITTEN_TAG) {
      last_written_pg = i + start_page;
    }
  }

  prv_flash_erase_sector(start_page);
  prv_write_erased_header_on_page_range(start_page,
      start_page + PFS_PAGES_PER_ERASE_SECTOR, max_erase);

  if (last_written_pg != INVALID_PAGE) {
    hdr.last_written = LAST_WRITTEN_TAG;
    prv_flash_write((uint8_t *)&hdr.last_written, sizeof(hdr.last_written),
        prv_page_to_flash_offset(last_written_pg) +
        offsetof(PageHeader, last_written));
  }
}

static bool prv_update_gc_reserved_region(void) {
  if (!s_gc_block.block_valid || (s_gc_block.block_writes > 5)) {
    int free_region_start = prv_find_free_erase_region(s_gc_block.block_valid);

    if (free_region_start >= 0) {
      s_gc_block = (GCBlock) {
        .block_valid = true,
        .block_writes = 0,
        .gc_start_page = free_region_start
      };
      PBL_LOG(LOG_LEVEL_DEBUG, "New Erase Region: %d", s_gc_block.gc_start_page);
      return (true);
    }

    return (false);
  }

  return (true); // gc block must be valid to get here
}

static bool watch_list_find_str(ListNode *node, void *data) {
  PFSFileChangedCallbackNode *filechg_node = (PFSFileChangedCallbackNode *)node;
  return (strcmp(filechg_node->name, (char *)data) == 0);
}

PFSCallbackHandle pfs_watch_file(const char* filename, PFSFileChangedCallback callback,
                                 uint8_t event_flags, void* data) {
  mutex_lock_recursive(s_pfs_mutex);

  PFSFileChangedCallbackNode *node = kernel_malloc_check(sizeof(PFSFileChangedCallbackNode));
  *node = (PFSFileChangedCallbackNode) {
    .callback = callback,
    .event_flags = event_flags,
    .data = data
  };

  // find out if we already have a string for this particular filename
  ListNode *find_str = list_find(s_head_callback_node_list, watch_list_find_str, (char *)filename);
  if (find_str == NULL) {
    node->name = kernel_strdup_check(filename);
  } else {
    node->name = ((PFSFileChangedCallbackNode *)find_str)->name;
  }

  s_head_callback_node_list = list_prepend(s_head_callback_node_list, &node->list_node);
  mutex_unlock_recursive(s_pfs_mutex);

  return node;
}

void pfs_unwatch_file(PFSCallbackHandle cb_handle) {
  mutex_lock_recursive(s_pfs_mutex);

  PFSFileChangedCallbackNode *callback_node = (PFSFileChangedCallbackNode *)cb_handle;

  PBL_ASSERTN(callback_node->list_node.next != NULL || callback_node->list_node.prev != NULL
              || s_head_callback_node_list == &callback_node->list_node);
  PBL_ASSERTN(list_contains(s_head_callback_node_list, &callback_node->list_node));
  list_remove(&callback_node->list_node, &(s_head_callback_node_list), NULL);

  // if no one is watching the file anymore, free the string
  ListNode *find_str = list_find(s_head_callback_node_list, watch_list_find_str,
      (char *)callback_node->name);
  if (find_str == NULL) {
    kernel_free((void *)(callback_node->name));
  }

  kernel_free(callback_node);

  mutex_unlock_recursive(s_pfs_mutex);
}

// IMPORTANT: This call assumes that the caller has already grabbed s_pfs_mutex
static void prv_invoke_watch_file_callbacks(const char* file_name, uint8_t event) {
  PFSFileChangedCallbackNode *callback_node =
                              (PFSFileChangedCallbackNode *)s_head_callback_node_list;
  while (callback_node) {
    if (!strcmp(callback_node->name, file_name) && (callback_node->event_flags & event)) {
      callback_node->callback(callback_node->data);
    }
    callback_node = (PFSFileChangedCallbackNode *)list_get_next(&callback_node->list_node);
  }
}

status_t pfs_close(int fd) {
  mutex_lock_recursive(s_pfs_mutex);

  int res = E_UNKNOWN;
  if (!FD_VALID(fd)) {
    res = E_INVALID_ARGUMENT;
    goto cleanup;
  }

  File *f = &PFS_FD(fd).file;
  if (f->is_tmp) {
    // Note: file_found_in_cache() now prevents opening a non-temp file while a
    // temp file with the same name is in use, so pfs_remove() here is safe.
    pfs_remove(f->name);
    // Note: if we reboot before updating the tmp state flag to done, the tmp &
    // original file will be deleted. This is an extremely small window, but
    // could be resolved by checking on reboot to see if both versions exist.
    // If both exist, the orig is valid. Iff tmp exists, the tmp file is valid
    update_curr_state(f->start_page, TMP_STATE_OFFSET, TMP_STATE_DONE);
    f->is_tmp = false;
  }

  // Note: We don't free f->name here because we keep the file metadata
  // (including the name, so we can detect hits) in the cache until we
  // have to evict it to make room for a new file.

  PFS_FD(fd).fd_status = FD_STATUS_UNREFERENCED;
  PFS_FD(fd).time_closed = time_closed_counter++;

  // If this file was modified, invoke the callbacks
  if (f->op_flags & (OP_FLAG_WRITE | OP_FLAG_OVERWRITE)) {
    // IMPORTANT: prv_invoke_watch_file_callbacks assumes that we already have s_pfs_mutex
    prv_invoke_watch_file_callbacks(f->name, FILE_CHANGED_EVENT_CLOSED);
  }

  res = S_SUCCESS;
cleanup:
  mutex_unlock_recursive(s_pfs_mutex);
  return (res);
}

status_t pfs_close_and_remove(int fd) {
  mutex_lock_recursive(s_pfs_mutex);

  status_t res = E_UNKNOWN;
  if (!FD_VALID(fd)) {
    res = E_INVALID_ARGUMENT;
  } else {
    File *f = &PFS_FD(fd).file;
    char file_name[f->namelen + 1];
    file_name[f->namelen] = '\0';
    memcpy(file_name, f->name, f->namelen);

    if ((res = pfs_close(fd)) >= 0) {
      res = pfs_remove(file_name);
    }
  }

  mutex_unlock_recursive(s_pfs_mutex);
  return (res);
}

status_t pfs_remove(const char *name) {
  if (name == NULL) {
    return E_INVALID_ARGUMENT;
  }
  size_t namelen = strlen(name);
  if ((namelen < 1) || (namelen > FILE_MAX_NAME_LEN)) {
    return (E_INVALID_ARGUMENT);
  }

  mutex_lock_recursive(s_pfs_mutex);
  uint16_t page = 0;
  int fd;
  status_t rv = get_avail_fd(name, &fd, false);
  if (rv >= FDAlreadyLoaded) { // the file is in the cache
    if (rv == FDBusy) {
      PBL_CROAK("Cannot delete %s, it is currently in use",
                PFS_FD(fd).file.name);
    }
    page = PFS_FD(fd).file.start_page;
    mark_fd_free(fd);
  } else if ((rv = locate_flash_file(name, &page)) != S_SUCCESS) {
    goto cleanup; // could not find the file on flash
  }

  rv = unlink_flash_file(page);
  // IMPORTANT: prv_invoke_watch_file_callbacks assumes that we already have s_pfs_mutex
  prv_invoke_watch_file_callbacks(name, FILE_CHANGED_EVENT_REMOVED);
cleanup:
  mutex_unlock_recursive(s_pfs_mutex);
  return (rv);
}

PFSFileListEntry *pfs_create_file_list(PFSFilenameTestCallback callback) {
  ListNode *head = NULL;

  mutex_lock_recursive(s_pfs_mutex);

  const int file_namelen_offset = FILEHEADER_OFFSET + offsetof(FileHeader, file_namelen);

  for (uint16_t pg = 0; pg < s_pfs_page_count; pg++) {
    PageHeader pg_hdr;
    pg_hdr.page_flags = prv_get_page_flags(pg);

    if (!IS_PAGE_TYPE(pg_hdr.page_flags, PAGE_FLAG_START_PAGE)) {
      continue; // only start pages contain file name info
    }

    FileHeader file_hdr;
    prv_flash_read((uint8_t *)&file_hdr.file_namelen, sizeof(file_hdr.file_namelen),
                   prv_page_to_flash_offset(pg) + file_namelen_offset);

    char file_name[file_hdr.file_namelen + 1];
    prv_flash_read((uint8_t *)file_name, file_hdr.file_namelen,
                   prv_page_to_flash_offset(pg) + FILE_NAME_OFFSET);
    file_name[file_hdr.file_namelen] = 0;

    if (callback && !callback(file_name)) {
      // Don't include
      continue;
    }

    // Make sure the rest of the page header contents are valid. We are doing this after the
    // filename filter call because it requires more flash reads and is likely slower than the
    // filter call.
    if (read_header(pg, &pg_hdr, &file_hdr) != PageAndFileHdrValid) {
      PBL_LOG(LOG_LEVEL_WARNING, "%d: Invalid page/file header", pg);
      continue;
    }

    // Add a new entry
    PFSFileListEntry *entry = kernel_malloc_check(sizeof(PFSFileListEntry)
                                                  + file_hdr.file_namelen + 1);
    *entry = (PFSFileListEntry) {};
    strcpy(entry->name, file_name);
    head = list_insert_before(head, &entry->list_node);
  }
  mutex_unlock_recursive(s_pfs_mutex);
  return (PFSFileListEntry *)head;
}

void pfs_delete_file_list(PFSFileListEntry *head) {
  ListNode *node = (ListNode *)head;
  ListNode *next;
  while (node) {
    next = node->next;
    kernel_free(node);
    node = next;
  }
}

// PBL-19098 Refactor this to share code with pfs_create_file_list
void pfs_remove_files(PFSFilenameTestCallback callback) {
  mutex_lock_recursive(s_pfs_mutex);

  const int file_namelen_offset = FILEHEADER_OFFSET + offsetof(FileHeader, file_namelen);

  for (uint16_t pg = 0; pg < s_pfs_page_count; pg++) {
    PageHeader pg_hdr;
    pg_hdr.page_flags = prv_get_page_flags(pg);

    if (!IS_PAGE_TYPE(pg_hdr.page_flags, PAGE_FLAG_START_PAGE)) {
      continue; // only start pages contain file name info
    }

    FileHeader file_hdr;
    prv_flash_read((uint8_t *)&file_hdr.file_namelen, sizeof(file_hdr.file_namelen),
                   prv_page_to_flash_offset(pg) + file_namelen_offset);

    char file_name[file_hdr.file_namelen + 1];
    prv_flash_read((uint8_t *)file_name, file_hdr.file_namelen,
                   prv_page_to_flash_offset(pg) + FILE_NAME_OFFSET);
    file_name[file_hdr.file_namelen] = 0;

    if (callback && !callback(file_name)) {
      // Don't include
      continue;
    }

    // Make sure the rest of the page header contents are valid. We are doing this after the
    // filename filter call because it requires more flash reads and is likely slower than the
    // filter call.
    if (read_header(pg, &pg_hdr, &file_hdr) != PageAndFileHdrValid) {
      PBL_LOG(LOG_LEVEL_WARNING, "%d: Invalid page/file header", pg);
      continue;
    }

    int fd;
    status_t rv = get_avail_fd(file_name, &fd, false);
    if (rv >= FDAlreadyLoaded) { // the file is in the cache
      if (rv == FDBusy) {
        PBL_CROAK("Cannot delete %s, it is currently in use",
                  s_pfs_avail_fd[fd].file.name);
      }
      mark_fd_free(fd);
    }

    unlink_flash_file(pg);
    // IMPORTANT: prv_invoke_watch_file_callbacks assumes that we already have s_pfs_mutex
    prv_invoke_watch_file_callbacks(file_name, FILE_CHANGED_EVENT_REMOVED);
  }
  mutex_unlock_recursive(s_pfs_mutex);
}

#define MAX_PAGE_CACHE_ENTRIES    10 // 6 bytes per entry
static void update_page_cache(FilePageCache *fpc, int *cur_idx,
    FilePageCache *toadd) {

  int optimal_idx = *cur_idx;
  if ((*cur_idx) == MAX_PAGE_CACHE_ENTRIES) {
    // default index to overwrite if nothing better is found
    optimal_idx = MAX_PAGE_CACHE_ENTRIES - 1;

    uint8_t contiguous_pgs = fpc[0].contiguous_pgs;

    // find the entry with the smallest number of sequential pages as this
    // will be the best page to remove from the cache
    for (int i = 0; i < MAX_PAGE_CACHE_ENTRIES; i++) {
      if (fpc[i].contiguous_pgs < contiguous_pgs) {
        optimal_idx = i;
        contiguous_pgs = fpc[i].contiguous_pgs;
      }
    }

    // only kick the current cache entry if it's worse than the one
    // we are adding
    if (fpc[optimal_idx].contiguous_pgs > toadd->contiguous_pgs) {
      return;
    }
  } else {
    (*cur_idx)++; // we are adding a new entry
  }

  fpc[optimal_idx] = *toadd;
}

static NOINLINE void allocate_page_cache(int fd) {
  File *f = &PFS_FD(fd).file;

  if (f->pg_cache != NULL) {
    return;  // already cached
  }

  if ((f->file_size / free_bytes_in_page(f->start_page)) < 1) {
    return; // only one page in use so we don't need to cache anything
  }

  // Note: If there was more space for statics or stack space we could
  // put this temporary buffer there
  FilePageCache *fpc =
      kernel_malloc_check(sizeof(FilePageCache) * MAX_PAGE_CACHE_ENTRIES);
  memset(fpc, 0x00, sizeof(FilePageCache) * MAX_PAGE_CACHE_ENTRIES);

  uint16_t virtual_pg = 0;
  uint16_t curr_page = f->start_page;
  uint16_t next_page;
  int cur_idx = 0;

  FilePageCache curr = {
    .virtual_pg = 0,
    .physical_pg = f->start_page,
    .contiguous_pgs = 0
  };

  while (get_next_page(curr_page, &next_page) == S_SUCCESS) {
    if (next_page == (curr_page + 1)) {
      curr.contiguous_pgs++;
    } else {
      update_page_cache(&fpc[0], &cur_idx, &curr);

      // reset logic for next entry
      curr.virtual_pg = virtual_pg + 1;
      curr.physical_pg = next_page;
      curr.contiguous_pgs = 0;
    }

    curr_page = next_page;
    virtual_pg++;
  }

  // see if the last set should be added to the cache
  update_page_cache(&fpc[0], &cur_idx, &curr);

  // The cache is likely to be around for a while and there is no reason to
  // burn up more memory than necessary for a long duration
  f->pg_cache = kernel_malloc(sizeof(FilePageCache) * cur_idx);
  if (f->pg_cache != NULL) { // if we are not OOM
    memcpy(f->pg_cache, fpc, sizeof(FilePageCache) * cur_idx);
    f->pg_cache_len = cur_idx;
  }

  kernel_free(fpc);
}

///
/// Helper routines for pfs_open()
///

//! Returns true iff the file is found in the cache and the fd is ready to use
//! fd_used >= 0 if we were able to allocate a fd for the file (regardless of
//! whether or not its in the cache), else it reflects the error code
static NOINLINE bool file_found_in_cache(const char *name, uint8_t op_flags, int *fd_used) {
  int fd, res;
  bool is_tmp = ((op_flags & OP_FLAG_OVERWRITE) != 0);
  bool file_found = false;

  if ((res = get_avail_fd(name, &fd, is_tmp)) == NoFDAvail) {
    res = E_OUT_OF_RESOURCES;
    goto cleanup;
  } else if (res == FDBusy) {
    res = E_BUSY; // the file is already open
    goto cleanup;
  }

  // When opening a non-temp file, check if a temp file with the same name is in use.
  // If so, block the open to prevent crashes when the temp file is later closed
  // (pfs_close for temp files calls pfs_remove on the original filename).
  if (!is_tmp) {
    int tmp_fd;
    if (get_avail_fd(name, &tmp_fd, true) == FDBusy) {
      res = E_BUSY;
      goto cleanup;
    }
  }

  File *file = &PFS_FD(fd).file;

  // settings for cached & new fds
  file->op_flags = op_flags;
  file->offset = 0; // (re)set seek position
  file->is_tmp = is_tmp;

  if (res == FDAlreadyLoaded) { // we found the FD in cache!
    file->curr_page = file->start_page;

    bool perform_crc_check = (op_flags & OP_FLAG_SKIP_HDR_CRC_CHECK) == 0;
    if (perform_crc_check) {
      // make sure the header is not corrupted
      PageHeader pg_hdr;
      FileHeader file_hdr;
      if ((res = read_header(file->start_page, &pg_hdr, &file_hdr)) !=
          PageAndFileHdrValid) {
        mark_fd_free(fd); // file has been corrupted so clear fd
        goto cleanup;
      }
    }

    PFS_FD(fd).fd_status = FD_STATUS_IN_USE;
    file_found = true;
  }

cleanup:
  *fd_used = (res >= 0) ? fd : res;
  return (file_found);
}

// handles the creation of a file which was not previously on the FS
static NOINLINE status_t pfs_open_handle_create_request(int fd, uint8_t file_type,
    size_t start_size) {

  if (!VALID_TYPE(file_type) || (start_size == 0)) {
    return (E_INVALID_ARGUMENT);
  }

  File *file = &PFS_FD(fd).file;
  file->file_size = start_size;
  file->file_type = file_type;

  // temporarily mark the file as in use so no one tries to use the fd once we
  // release the lock
  if (fd != GC_FD_HANDLE_ID) {
    FileDesc *file_desc = &PFS_FD(fd);
    uint8_t curr_status = file_desc->fd_status;
    file_desc->fd_status = FD_STATUS_IN_USE;
    mutex_unlock_recursive(s_pfs_mutex);
    pfs_prepare_for_file_creation(start_size, 0 /* no timeout */);
    mutex_lock_recursive(s_pfs_mutex);
    file_desc->fd_status = curr_status;
  }

  int res = create_flash_file(file);
  return (res);
}

// given the fd and start page of a file, loads file description with relevent
// info about file so it can be read from
static NOINLINE status_t pfs_open_handle_read_request(int fd, uint16_t page) {
  PageHeader pg_hdr;
  FileHeader file_hdr;
  int hdr_rv;

  if ((hdr_rv = read_header(page, &pg_hdr, &file_hdr)) == PageAndFileHdrValid) {
    File *file = &PFS_FD(fd).file;
    file->file_size = file_hdr.file_size;
    file->file_type = file_hdr.file_type;
    file->start_page = file->curr_page = page;
    return (S_SUCCESS);
  }

  PBL_LOG(LOG_LEVEL_WARNING, "Could not read header %d", hdr_rv);
  return (E_INTERNAL);
}

static int file_found_or_added_to_pfs(int fd, const char *name,
    uint8_t op_flags, uint8_t file_type, size_t start_size) {

  uint16_t page = 0;
  int res = locate_flash_file(name, &page);

  if ((res != S_SUCCESS) && (res != E_DOES_NOT_EXIST)) { // unexpected error
    goto cleanup;
  }

  // check to see if we are trying to read the file and it doesn't exist
  bool is_read_only = (op_flags & (OP_FLAG_READ | OP_FLAG_WRITE |
      OP_FLAG_OVERWRITE)) == OP_FLAG_READ;
  bool is_tmp = ((op_flags & OP_FLAG_OVERWRITE) != 0);
  if ((is_read_only || is_tmp) && (res == E_DOES_NOT_EXIST)) {
    goto cleanup;
  }

  // Prepare the new FD
  FileDesc *file_desc = &PFS_FD(fd);
  File *file = &PFS_FD(fd).file;

  file_desc->fd_status = FD_STATUS_UNREFERENCED; // set to IN_USE on success
  if ((file->name = kernel_strdup(name)) == NULL) {
    res = E_OUT_OF_MEMORY;
    goto cleanup;
  }
  file->namelen = strlen(name);
  file->start_offset = FILEDATA_LEN + file->namelen;

  if (is_tmp || ((res == E_DOES_NOT_EXIST) && ((op_flags & OP_FLAG_WRITE) != 0))) {
    res = pfs_open_handle_create_request(fd, file_type, start_size);
  } else if ((op_flags & OP_FLAG_READ) != 0) {
    res = pfs_open_handle_read_request(fd, page);
  } else { // unexpected situation
    res = E_INTERNAL;
  }

cleanup:
  if (res < S_SUCCESS) {
    mark_fd_free(fd);
  } else {
    PFS_FD(fd).fd_status = FD_STATUS_IN_USE;
  }
  return (res);
}

int pfs_open(const char *name, uint8_t op_flags, uint8_t file_type,
    size_t start_size) {

  size_t namelen = (name == NULL) ? 0 : strlen(name);
  if ((namelen < 1) || (namelen > FILE_MAX_NAME_LEN)) {
    return (E_INVALID_ARGUMENT);
  }

  mutex_lock_recursive(s_pfs_mutex);
  int res;
  // if the file is in the cache or we encountered a failure we are done
  if (file_found_in_cache(name, op_flags, &res) || (res < S_SUCCESS)) {
    goto cleanup;
  }

  // The file is not in the cache, let's see if it's on the filesystem
  int fd = res;
  if ((res = file_found_or_added_to_pfs(fd, name, op_flags, file_type,
      start_size)) >= S_SUCCESS) {
    res = fd; // success so return the fd
  }

cleanup:
  if (res >= S_SUCCESS) {
    // we are returning a valid file handle so if the user has asked for the
    // page translations to be cached let's do that now
    if ((op_flags & OP_FLAG_USE_PAGE_CACHE) != 0) {
        allocate_page_cache(res);
    }
    // check to see if we should update the gc block
    prv_update_gc_reserved_region();
  }

  mutex_unlock_recursive(s_pfs_mutex);
  return (res);
}

static int pfs_open_gc_file(uint32_t space_needed, bool create) {
  int fd = GC_FD_HANDLE_ID; // the gc fd follows the avail fd
  File *file = &PFS_FD(fd).file;

  // settings for cached & new fds
  file->op_flags = OP_FLAG_READ;
  if (create) {
    file->op_flags |= OP_FLAG_WRITE;
  }
  file->offset = 0; // (re)set seek position
  file->is_tmp = false;

  if (s_gc_block.block_valid && create) {
    prv_flash_erase_sector(s_gc_block.gc_start_page);
  }

  int res = file_found_or_added_to_pfs(fd, GC_FILE_NAME, file->op_flags,
      FILE_TYPE_STATIC, space_needed);

  PBL_ASSERTN(!create || res >= 0); // we are toast if we cannot create the file
  return (res >= 0) ? fd : res;
}

static status_t copy_or_recover_gc_data(int fd, GCData *gcdata, bool do_copy) {
  //////
  //
  // GC File Format
  //
  // GCData
  // Page 0 Header | Data Len | Data
  // ...
  // Page N Header | Data Len | Data
  //
  //////
  uint32_t sector_start_page = gcdata->gc_start_page;
  uint32_t sectors_active = gcdata->page_mask;

  // copy the entire block to file (could use bss?)
  const size_t copy_buf_size = 256;
  uint8_t *buf = kernel_malloc_check(copy_buf_size);

  for (uint16_t pg = 0; pg < PFS_PAGES_PER_ERASE_SECTOR; pg++) {
    uint32_t base_addr = prv_page_to_flash_offset(sector_start_page + pg);

    uint32_t data_len;
    PageHeader hdr;
    if (do_copy) {
      // if the sector is not active we only need to copy the page header info
      data_len = (((sectors_active >> pg) & 0x1) == 0) ?
          0 : PFS_PAGE_SIZE - sizeof(PageHeader);
      if (data_len == 0) {
        get_updated_erase_hdr(&hdr, sector_start_page + pg);
      } else {
        prv_flash_read((uint8_t *)&hdr, sizeof(hdr), base_addr);
      }
    }

    // Write Page Header + DataLen
    if (do_copy) {
      pfs_write(fd, &hdr, sizeof(hdr));
      pfs_write(fd, &data_len, sizeof(data_len));
    } else { // recover
      pfs_read(fd, &hdr, sizeof(hdr));
      pfs_read(fd, &data_len, sizeof(data_len));
      prv_flash_write(&hdr, sizeof(hdr), base_addr);
    }

    base_addr += sizeof(PageHeader);
    for (int i = 0; i < (int)data_len; i += copy_buf_size) {
      size_t to_copy = MIN(data_len - i, copy_buf_size);
      if (do_copy) {
        prv_flash_read(buf, to_copy, base_addr + i);
        pfs_write(fd, buf, to_copy);
      } else {
        pfs_read(fd, buf, to_copy);
        prv_flash_write(buf, to_copy, base_addr + i);
      }
    }
  }

  kernel_free(buf);
  return (S_SUCCESS);
}

static void recover_region_from_file(int fd) {
  GCData gcdata;

  pfs_seek(fd, 0, FSeekSet);
  pfs_read(fd, &gcdata, sizeof(gcdata));

  if (!GCDATA_VALID(gcdata.flags)) {
    // we never completed setting up the migration
    goto done;
  }

  // at this point we can erase the block
  prv_handle_sector_erase(gcdata.gc_start_page, false);

  copy_or_recover_gc_data(fd, &gcdata, false);

done:
  pfs_close_and_remove(fd);
}

static int prv_copy_sector_to_gc_file(uint16_t *free_page,
    uint16_t sector_start_page, uint32_t sectors_active) {
  size_t num_entries = __builtin_popcount(sectors_active);

  // We need space to store all the data for active pages, the
  // page header for all pages, and the GCData struct
  size_t space_needed = 0;
  space_needed += (num_entries * (PFS_PAGE_SIZE - AVAIL_BYTES_OFFSET));
  space_needed += (PFS_PAGES_PER_ERASE_SECTOR * (sizeof(PageHeader) + 4));
  space_needed += sizeof(GCData);

  // we rely on having 1 page to store some metadata so make sure
  // we always have enough space based on our block & erase size
  _Static_assert((PFS_PAGES_PER_ERASE_SECTOR * (sizeof(PageHeader) + 4)) <
      (PFS_PAGE_SIZE - AVAIL_BYTES_OFFSET), "Too many pages per Erase sector");
  PBL_ASSERTN(num_entries < PFS_PAGES_PER_ERASE_SECTOR);

  int fd = pfs_open_gc_file(space_needed, true);
  GCData gcdata = {
    .version = 0, // Version 0 for now, bump if we change
    .flags = 0xff,
    .gc_start_page = sector_start_page,
    .num_entries = num_entries,
    .page_mask = sectors_active
  };

  // write out the GCData to the file
  pfs_write(fd, &gcdata, sizeof(gcdata));

  // copy all the data we need to the file
  copy_or_recover_gc_data(fd, &gcdata, true);

  // mark our data as valid
  gcdata.flags &= ~GC_DATA_VALID;
  pfs_seek(fd, offsetof(GCData, flags), FSeekSet);
  pfs_write(fd, &gcdata.flags, sizeof(gcdata.flags));

  return (fd);
}

static NOINLINE status_t garbage_collect_sector(uint16_t *free_page,
    uint16_t sector_start_page, uint32_t sectors_active) {

  // if no sectors are active in the region, just erase it!
  if (sectors_active == 0) {
    prv_handle_sector_erase(sector_start_page, true);
    goto done;
  }

  int fd = prv_copy_sector_to_gc_file(free_page, sector_start_page,
      sectors_active);

  recover_region_from_file(fd);

  // we used the gc block
  s_gc_block.block_writes++;

done:
  for (uint16_t pg = 0; pg < PFS_PAGES_PER_ERASE_SECTOR; pg++) {
    if (((sectors_active >> pg) & 0x1) == 0) {
      *free_page = pg + sector_start_page;
      return (S_SUCCESS);
    }
  }

  return (E_INTERNAL);
}

status_t pfs_init(bool run_filesystem_check) {
  if (s_pfs_mutex == NULL) {
    s_pfs_mutex = mutex_create_recursive();
  }

  for (int fd = FD_INDEX_OFFSET; fd < FD_INDEX_OFFSET+MAX_FD_HANDLES; fd++) {
    PFS_FD(fd) = (FileDesc) { .fd_status = FD_STATUS_FREE };
  }

  ftl_populate_region_list();

  if (run_filesystem_check) {
    if (!pfs_active()) {
      // either we have downgraded or there is no data on the flash
      PBL_LOG(LOG_LEVEL_INFO, "PFS not active ... formatting");
      pfs_format(true /* write erase headers */);
    }
  }

  // we need to run this before reserving a new GC region so that we don't
  // think a region is free when in reality we just rebooted in the middle of it
  // being re-written
  int fd;
  if ((fd = pfs_open_gc_file(0, false)) >= S_SUCCESS) {
    // we rebooted while we were in the middle of a garbage collection
    PBL_LOG(LOG_LEVEL_INFO, "Recovering flash region from GC file");
    recover_region_from_file(fd);
  }

  // find a free region
  if (!prv_update_gc_reserved_region()) {
    PBL_LOG(LOG_LEVEL_ERROR, "No free flash erase units!");
    // Note: It should not be possible for this to happen since start of day no
    // files will be written on then flash. We could also try to force apps to
    // be flushed out of the FS in an attempt to free up space since they are
    // only being cached on the FS
    pfs_format(true);
  }

  // get us off to a good start by ensuring there is some pre-erased space on
  // the filesystem. We do a lot of initialization from different threads early
  // during boot flow. This prevents those threads from blocking each other
  uint32_t bytes_to_free = ((s_pfs_page_count * PFS_PAGE_SIZE) * 4) / 100;
  PBL_LOG(LOG_LEVEL_DEBUG, "Preparing %"PRIu32" bytes of flash for filesystem use",
          bytes_to_free);

  pfs_prepare_for_file_creation(bytes_to_free, 15 * RTC_TICKS_HZ);

  return (S_SUCCESS);
}

void pfs_format(bool write_erase_headers) {
  PBL_LOG(LOG_LEVEL_INFO, "FS-Format Start");
  mutex_lock_recursive(s_pfs_mutex);

  for (int i = FD_INDEX_OFFSET; i < FD_INDEX_OFFSET+PFS_FD_SET_SIZE; i++) {
    mark_fd_free(i);
  }

  // clear out all pages
  filesystem_regions_erase_all();
  prv_invalidate_page_flags_cache_all();

  if (write_erase_headers) {
    prv_write_erased_header_on_page_range(0, s_pfs_page_count, 1);
  }

  mutex_unlock_recursive(s_pfs_mutex);
  PBL_LOG(LOG_LEVEL_INFO, "FS-Format Done");
}


int pfs_sector_optimal_size(int min_size, int namelen) {
  min_size += sizeof(FileHeader);
  min_size += sizeof(FileMetaData);
  min_size += namelen;

  int bytes_per_sector = PFS_PAGE_SIZE - sizeof(PageHeader);
  int num_pages = min_size / bytes_per_sector;
  if ((min_size % bytes_per_sector) > 0) {
    num_pages++;
  }
  int optimal_size = num_pages * bytes_per_sector;

  optimal_size -= sizeof(FileHeader);
  optimal_size -= sizeof(FileMetaData);
  optimal_size -= namelen;
  return optimal_size;
}

uint32_t get_available_pfs_space(void) {
  uint32_t allocated_space = 0;

  for (uint16_t pg = 0; pg < s_pfs_page_count; pg++) {
    uint8_t page_flags = prv_get_page_flags(pg);

    if ((IS_PAGE_TYPE(page_flags, PAGE_FLAG_START_PAGE)) ||
        (IS_PAGE_TYPE(page_flags, PAGE_FLAG_CONT_PAGE))) {
      allocated_space += free_bytes_in_page(pg);
    }
  }

  // A full filesystem is bad for wear leveling since the same sectors will
  // wind up getting written repeatedly. We should really be enforcing this
  // within pfs_open but for now we will just let external callers use this
  // routine before allocating large files
  uint32_t tot_capacity = (pfs_get_size() * 8) / 10;

  return ((allocated_space >= tot_capacity) ?
          0 : (tot_capacity - allocated_space));
}

uint32_t pfs_crc_calculate_file(int fd, uint32_t offset, uint32_t num_bytes) {
  LegacyChecksum checksum;
  legacy_defective_checksum_init(&checksum);

  // grab the pfs lock to prevent lock inversion with crc lock
  mutex_lock_recursive(s_pfs_mutex);

  // go to offset
  pfs_seek(fd, offset, FSeekSet);
  const unsigned int chunk_size = 128;
  uint8_t buffer[chunk_size];

  while (num_bytes > chunk_size) {
    pfs_read(fd, buffer, chunk_size);
    legacy_defective_checksum_update(&checksum, buffer, chunk_size);
    num_bytes -= chunk_size;
  }

  pfs_read(fd, buffer, num_bytes);
  legacy_defective_checksum_update(&checksum, buffer, num_bytes);
  uint32_t crc = legacy_defective_checksum_finish(&checksum);

  mutex_unlock_recursive(s_pfs_mutex);

  return (crc);
}

void analytics_external_collect_pfs_stats(void) {
  uint16_t avail_kilobytes = (uint16_t)(get_available_pfs_space() / 1024);
  analytics_set(ANALYTICS_DEVICE_METRIC_PFS_SPACE_FREE_KB,
                avail_kilobytes, AnalyticsClient_System);
}

/*
 * Debug Utilities
 */

// TODO: Remove once we figure out PBL-20973
void pfs_collect_diagnostic_data(int fd, void *diagnostic_buf, size_t diagnostic_buf_len)  {
  mutex_lock_recursive(s_pfs_mutex);
  memcpy(diagnostic_buf, &PFS_FD(fd), MIN(diagnostic_buf_len, sizeof(FileDesc)));
  mutex_unlock_recursive(s_pfs_mutex);
}

// pass in either 0 or 1 to as argument
void pfs_command_fs_format(const char *erase_headers) {
  int write_erase_headers = atoi(erase_headers);
  if (write_erase_headers == 1) {
    pfs_format(true /* write erase headers */);
  } else {
    pfs_format(false /* write erase headers */);
  }
}

void pfs_command_dump_hdr(const char *page) {
  uint16_t pg = (uint16_t) atoi(page);
  if (pg > s_pfs_page_count) {
    prompt_send_response("ERROR");
    return;
  }

  uint8_t hdr[FILE_NAME_OFFSET + 10];
  prv_flash_read((uint8_t *)&hdr, sizeof(hdr), prv_page_to_flash_offset(pg));

  PBL_HEXDUMP_D_SERIAL(LOG_LEVEL_DEBUG, hdr, sizeof(hdr));
}

void pfs_command_fs_ls(void) {
  char display_buf[80];
  int pages_in_use = 0;

  prompt_send_response("Page:\tFilename\tFile Size\tFile Info\tErase Count\n");

  for (uint16_t pg = 0; pg < s_pfs_page_count; pg++) {
    PageHeader pg_hdr;
    FileHeader file_hdr;
    pg_hdr.page_flags = prv_get_page_flags(pg);

    if (!IS_PAGE_TYPE(pg_hdr.page_flags, PAGE_FLAG_START_PAGE)) {
      pages_in_use += IS_PAGE_TYPE(pg_hdr.page_flags,
          PAGE_FLAG_CONT_PAGE) ? 1 : 0;
      continue; // only start pages contain file name info
    }
    pages_in_use++;

    if (read_header(pg, &pg_hdr, &file_hdr) != PageAndFileHdrValid) {
      snprintf(display_buf, sizeof(display_buf), "%3d: Corrupt Sector", pg);
      prompt_send_response(display_buf);
    }

    char file_name[file_hdr.file_namelen + 1];
    file_name[file_hdr.file_namelen] = '\0';

    prv_flash_read((uint8_t *)file_name, file_hdr.file_namelen,
        prv_page_to_flash_offset(pg) + FILE_NAME_OFFSET);

    snprintf(display_buf, sizeof(display_buf), "%3d:\t%8s%s\t%5d\t\t0x%x\t%15d",
        pg, file_name, is_tmp_file(pg) ? "(tmp)" : "", (int)file_hdr.file_size,
        file_hdr.file_type, (int)pg_hdr.erase_count);
    prompt_send_response(display_buf);
  }

  snprintf(display_buf, sizeof(display_buf), "\n---\n%d / %d pages in use "
      "(%"PRIu32" kB available)", pages_in_use, s_pfs_page_count,
       get_available_pfs_space() / 1024);
  prompt_send_response(display_buf);
}

// Dump the first n bytes of a file (from current seek position)
void pfs_debug_dump(int fd, int num_bytes) {
  char buf[16];
  uint8_t *bytes = kernel_malloc(num_bytes);

  if (bytes == NULL) {
    prompt_send_response("malloc error");
    goto cleanup;
  }

  memset(bytes, 0x00, num_bytes);
  if ((num_bytes = pfs_read(fd, bytes, num_bytes)) < 0) {
    prompt_send_response_fmt(buf, sizeof(buf), "rd err: %d", num_bytes);
    goto cleanup;
  }

  PBL_HEXDUMP_D_SERIAL(LOG_LEVEL_DEBUG, bytes, num_bytes);

  prompt_send_response("DONE");
cleanup:
  kernel_free(bytes);
}

void pfs_command_cat(const char *filename, const char *num_chars) {
  int fd = pfs_open(filename, OP_FLAG_READ, 0, 0);
  char buf[16];
  if (fd < 0) {
    prompt_send_response_fmt(buf, sizeof(buf), "fd open err: %d", fd);
    return;
  }
  int num_bytes = atoi(num_chars);
  pfs_debug_dump(fd, num_bytes);
  pfs_close(fd);
}

void pfs_command_crc(const char *filename) {
  int fd = pfs_open(filename, OP_FLAG_READ, 0, 0);
  char buffer[32];
  if (fd < 0) {
    prompt_send_response_fmt(buffer, sizeof(buffer), "fd open err: %d", fd);
    return;
  }
  size_t num_bytes = pfs_get_file_size(fd);
  uint32_t crc = pfs_crc_calculate_file(fd, 0, num_bytes);
  pfs_close(fd);
  prompt_send_response_fmt(buffer, sizeof(buffer), "CRC: %"PRIx32, crc);
}

/*
 * Routines to facilitate unit testing
 */
#if UNITTEST
uint16_t test_get_file_start_page(int fd) {
  return (PFS_FD(fd).file.start_page);
}

void test_force_garbage_collection(uint16_t start_page) {
  start_page  = (start_page / PFS_PAGES_PER_ERASE_SECTOR) * PFS_PAGES_PER_ERASE_SECTOR;

  uint16_t free_page;
  uint32_t active_sectors =
      prv_get_sector_page_status(start_page / PFS_PAGES_PER_ERASE_SECTOR , &free_page);

  garbage_collect_sector(&free_page, start_page, active_sectors);
}

status_t test_scan_for_last_written(void) {
  for (uint16_t pg = 0; pg < s_pfs_page_count; pg++) {
    PageHeader hdr;
    prv_flash_read((uint8_t *)&hdr.last_written, sizeof(hdr.last_written),
        prv_page_to_flash_offset(pg) + offsetof(PageHeader, last_written));
    if (hdr.last_written == LAST_WRITTEN_TAG) {
      return (pg);
    }
  }

  return (-1);
}

void test_force_recalc_of_gc_region(void) {
  s_gc_block.block_valid = false;
  prv_update_gc_reserved_region();
}

void test_force_reboot_during_garbage_collection(uint16_t start_page) {
  start_page =
      (start_page / PFS_PAGES_PER_ERASE_SECTOR) * PFS_PAGES_PER_ERASE_SECTOR;

  uint16_t free_page;
  uint32_t active_sectors = prv_get_sector_page_status(start_page, &free_page);

  prv_copy_sector_to_gc_file(&free_page, start_page, active_sectors);

  // blow away the sector
  prv_handle_sector_erase(s_gc_block.gc_start_page, false);
}

void test_override_last_written_page(uint16_t start_page) {
  s_test_last_page_written_override = s_last_page_written;
}
#endif
