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

#include "qspi_flash.h"

#include "board/board.h"
#include "drivers/flash/flash_impl.h"
#include "drivers/gpio.h"
#include "drivers/qspi.h"
#include "flash_region/flash_region.h"
#include "kernel/util/delay.h"
#include "kernel/util/sleep.h"
#include "mcu/cache.h"
#include "qspi_flash_definitions.h"
#include "system/logging.h"
#include "system/passert.h"
#include "system/status_codes.h"
#include "util/math.h"

#define STM32F4_COMPATIBLE
#define STM32F7_COMPATIBLE
#include <mcu.h>

#define FLASH_RESET_WORD_VALUE (0xffffffff)

static void prv_read_register(QSPIFlash *dev, uint8_t instruction, uint8_t *data, uint32_t length) {
  qspi_indirect_read_no_addr(dev->qspi, instruction, 0, data, length, false /* !is_ddr */);
}

static void prv_write_cmd_no_addr(QSPIFlash *dev, uint8_t cmd) {
  qspi_indirect_write_no_addr(dev->qspi, cmd, NULL, 0);
}

static void prv_write_enable(QSPIFlash *dev) {
  prv_write_cmd_no_addr(dev, dev->state->part->instructions.write_enable);
  // wait for writing to be enabled
  qspi_poll_bit(dev->qspi, dev->state->part->instructions.rdsr1,
                dev->state->part->status_bit_masks.write_enable, true /* set */, QSPI_NO_TIMEOUT);
}

static bool prv_check_whoami(QSPIFlash *dev) {
  // The WHOAMI is 3 bytes
  const uint32_t whoami_length = 3;
  uint32_t read_whoami = 0;
  prv_read_register(dev, dev->state->part->instructions.qspi_id, (uint8_t *)&read_whoami,
                    whoami_length);

  if (read_whoami == dev->state->part->qspi_id_value) {
    PBL_LOG(LOG_LEVEL_INFO, "Flash is %s", dev->state->part->name);
    return true;
  } else {
    PBL_LOG(LOG_LEVEL_ERROR, "Flash isn't expected %s (whoami: 0x%" PRIx32 ")",
            dev->state->part->name, read_whoami);
    return false;
  }
  qspi_release(dev->qspi);
}

bool qspi_flash_check_whoami(QSPIFlash *dev) {
  qspi_use(dev->qspi);
  bool result = prv_check_whoami(dev);
  qspi_release(dev->qspi);
  return result;
}

static void prv_set_fast_read_ddr_enabled(QSPIFlash *dev, bool enabled) {
  // If we're supposed to use DDR for fast read, make sure the part can support it
  PBL_ASSERTN(!enabled || dev->state->part->supports_fast_read_ddr);
  dev->state->fast_read_ddr_enabled = enabled;
}

bool qspi_flash_is_in_coredump_mode(QSPIFlash *dev) { return dev->state->coredump_mode; }

void qspi_flash_init(QSPIFlash *dev, QSPIFlashPart *part, bool coredump_mode) {
  dev->state->part = part;
  dev->state->coredump_mode = coredump_mode;
  prv_set_fast_read_ddr_enabled(dev, dev->default_fast_read_ddr_enabled);
  qspi_use(dev->qspi);

  if (dev->reset_gpio.gpio) {
    gpio_output_init(&dev->reset_gpio, GPIO_OType_PP, GPIO_Speed_2MHz);
    gpio_output_set(&dev->reset_gpio, false);
  }

  // Must call quad_enable first, all commands are QSPI
  qspi_indirect_write_no_addr_1line(dev->qspi, dev->state->part->instructions.enter_quad_mode);

  // Reset the flash to stop any program's or erase in progress from before reboot
  prv_write_cmd_no_addr(dev, dev->state->part->instructions.reset_enable);
  prv_write_cmd_no_addr(dev, dev->state->part->instructions.reset);

  if (coredump_mode) {
    delay_us(dev->state->part->reset_latency_ms * 1000);
  } else {
    psleep(dev->state->part->reset_latency_ms);
  }

  // Return the flash to Quad SPI mode, all our commands are quad-spi and it'll just cause
  // problems/bugs for someone if it comes back in single spi mode
  qspi_indirect_write_no_addr_1line(dev->qspi, dev->state->part->instructions.enter_quad_mode);

  if (!coredump_mode) {
    prv_check_whoami(dev);
  }

  qspi_release(dev->qspi);
}

status_t qspi_flash_is_erase_complete(QSPIFlash *dev) {
  qspi_use(dev->qspi);

  uint8_t status_reg;
  uint8_t flag_status_reg;
  prv_read_register(dev, dev->state->part->instructions.rdsr1, &status_reg, 1);
  prv_read_register(dev, dev->state->part->instructions.rdsr2, &flag_status_reg, 1);

  qspi_release(dev->qspi);

  if (status_reg & dev->state->part->status_bit_masks.busy) {
    return E_BUSY;
  } else if (flag_status_reg & dev->state->part->flag_status_bit_masks.erase_suspend) {
    return E_AGAIN;
  } else {
    return S_SUCCESS;
  }
}

status_t qspi_flash_erase_begin(QSPIFlash *dev, uint32_t addr, bool is_subsector) {
  uint8_t instruction;
  if (is_subsector) {
    instruction = dev->state->part->instructions.erase_sector_4k;
  } else {
    instruction = dev->state->part->instructions.erase_block_64k;
  }

  qspi_use(dev->qspi);
  prv_write_enable(dev);
  qspi_indirect_write(dev->qspi, instruction, addr, NULL, 0);
  // wait for busy to be set indicating the erase has started
  const uint32_t busy_timeout_us = 500;
  const bool result =
      qspi_poll_bit(dev->qspi, dev->state->part->instructions.rdsr1,
                    dev->state->part->status_bit_masks.busy, true /* set */, busy_timeout_us);
  qspi_release(dev->qspi);

  return result ? S_SUCCESS : E_ERROR;
}

status_t qspi_flash_erase_suspend(QSPIFlash *dev, uint32_t addr) {
  qspi_use(dev->qspi);

  uint8_t status_reg;
  prv_read_register(dev, dev->state->part->instructions.rdsr1, &status_reg, 1);
  if (!(status_reg & dev->state->part->status_bit_masks.busy)) {
    // no erase in progress
    qspi_release(dev->qspi);
    return S_NO_ACTION_REQUIRED;
  }

  prv_write_cmd_no_addr(dev, dev->state->part->instructions.erase_suspend);

  qspi_release(dev->qspi);

  if (dev->state->part->suspend_to_read_latency_us) {
    delay_us(dev->state->part->suspend_to_read_latency_us);
  }

  return S_SUCCESS;
}

void qspi_flash_erase_resume(QSPIFlash *dev, uint32_t addr) {
  qspi_use(dev->qspi);
  prv_write_cmd_no_addr(dev, dev->state->part->instructions.erase_resume);
  // wait for the erase_suspend bit to be cleared
  qspi_poll_bit(dev->qspi, dev->state->part->instructions.rdsr2,
                dev->state->part->flag_status_bit_masks.erase_suspend, false /* !set */,
                QSPI_NO_TIMEOUT);
  qspi_release(dev->qspi);
}

static void prv_get_fast_read_params(QSPIFlash *dev, uint8_t *instruction, uint8_t *dummy_cycles,
                                     bool *is_ddr) {
  if (dev->state->fast_read_ddr_enabled) {
    *instruction = dev->state->part->instructions.fast_read_ddr;
    *dummy_cycles = dev->state->part->dummy_cycles.fast_read_ddr;
    *is_ddr = true;
  } else {
    *instruction = dev->state->part->instructions.fast_read;
    *dummy_cycles = dev->state->part->dummy_cycles.fast_read;
    *is_ddr = false;
  }
}

static void prv_read_mmap_with_params(QSPIFlash *dev, uint32_t addr, void *buffer, uint32_t length,
                                      uint8_t instruction, uint8_t dummy_cycles, bool is_ddr) {
  qspi_mmap_start(dev->qspi, instruction, addr, dummy_cycles, length, is_ddr);

  // Point the buffer at the QSPI region
  memcpy(buffer, (uint32_t *)(QSPI_MMAP_BASE_ADDRESS + addr), length);

  // stop memory mapped mode
  qspi_mmap_stop(dev->qspi);
}

static void prv_read_mmap(QSPIFlash *dev, uint32_t addr, void *buffer, uint32_t length) {
  uint8_t instruction;
  uint8_t dummy_cycles;
  bool is_ddr;
  prv_get_fast_read_params(dev, &instruction, &dummy_cycles, &is_ddr);

  prv_read_mmap_with_params(dev, addr, buffer, length, instruction, dummy_cycles, is_ddr);
}

void qspi_flash_read_blocking(QSPIFlash *dev, uint32_t addr, void *buffer, uint32_t length) {
  // TODO: Figure out what thresholds we should use when switching between memory mapping, DMA, &
  // polling PBL-37438
  bool should_use_dma = length > 128 && !dev->state->coredump_mode;
  bool should_use_memmap = length > 128;

#if QSPI_DMA_DISABLE
  // Known issues with some platforms, see PBL-37278 as an example
  should_use_dma = false;
#endif

#if TARGET_QEMU
  // QEMU doesn't yet support DMA or memory-mapping
  should_use_dma = should_use_memmap = false;
#endif

  qspi_use(dev->qspi);
  uint8_t instruction;
  uint8_t dummy_cycles;
  bool is_ddr;
  prv_get_fast_read_params(dev, &instruction, &dummy_cycles, &is_ddr);
  if (should_use_dma) {
    qspi_indirect_read_dma(dev->qspi, instruction, addr, dummy_cycles, buffer, length, is_ddr);
  } else if (should_use_memmap) {
    prv_read_mmap_with_params(dev, addr, buffer, length, instruction, dummy_cycles, is_ddr);
  } else {
    qspi_indirect_read(dev->qspi, instruction, addr, dummy_cycles, buffer, length, is_ddr);
  }
  qspi_release(dev->qspi);
}

int qspi_flash_write_page_begin(QSPIFlash *dev, const void *buffer, uint32_t addr,
                                uint32_t length) {
  const uint32_t offset_in_page = addr % PAGE_SIZE_BYTES;
  const uint32_t bytes_in_page = MIN(PAGE_SIZE_BYTES - offset_in_page, length);

  qspi_use(dev->qspi);
  prv_write_enable(dev);
  qspi_indirect_write(dev->qspi, dev->state->part->instructions.pp, addr, buffer, bytes_in_page);
  qspi_poll_bit(dev->qspi, dev->state->part->instructions.rdsr1,
                dev->state->part->status_bit_masks.busy, false /* !set */, QSPI_NO_TIMEOUT);
  qspi_release(dev->qspi);

  return bytes_in_page;
}

status_t qspi_flash_get_write_status(QSPIFlash *dev) {
  qspi_use(dev->qspi);
  uint8_t status_reg;
  prv_read_register(dev, dev->state->part->instructions.rdsr1, &status_reg, 1);
  qspi_release(dev->qspi);
  return (status_reg & dev->state->part->status_bit_masks.busy) ? E_BUSY : S_SUCCESS;
}

void qspi_flash_set_lower_power_mode(QSPIFlash *dev, bool active) {
  qspi_use(dev->qspi);
  uint8_t instruction;
  uint32_t delay;
  if (active) {
    instruction = dev->state->part->instructions.enter_low_power;
    delay = dev->state->part->standby_to_low_power_latency_us;
  } else {
    instruction = dev->state->part->instructions.exit_low_power;
    delay = dev->state->part->low_power_to_standby_latency_us;
  }
  prv_write_cmd_no_addr(dev, instruction);
  qspi_release(dev->qspi);
  if (delay) {
    delay_us(delay);
  }
}

#if TARGET_QEMU
// While this works with normal hardware, it has a large stack requirment and I can't
// see a compelling reason to use it over the mmap blank check variant
static bool prv_blank_check_poll(QSPIFlash *dev, uint32_t addr, bool is_subsector) {
  const uint32_t size_bytes = is_subsector ? SUBSECTOR_SIZE_BYTES : SECTOR_SIZE_BYTES;
  const uint32_t BUF_SIZE_BYTES = 128;
  const uint32_t BUF_SIZE_WORDS = BUF_SIZE_BYTES / sizeof(uint32_t);
  uint32_t buffer[BUF_SIZE_WORDS];
  for (uint32_t offset = 0; offset < size_bytes; offset += BUF_SIZE_BYTES) {
    flash_impl_read_sync(buffer, addr + offset, BUF_SIZE_BYTES);
    for (uint32_t i = 0; i < BUF_SIZE_WORDS; ++i) {
      if (buffer[i] != FLASH_RESET_WORD_VALUE) {
        return false;
      }
    }
  }
  return true;
}
#endif

static bool prv_blank_check_mmap(QSPIFlash *dev, uint32_t addr, bool is_subsector) {
  const uint32_t size_bytes = is_subsector ? SUBSECTOR_SIZE_BYTES : SECTOR_SIZE_BYTES;
  bool result = true;
  uint8_t instruction;
  uint8_t dummy_cycles;
  bool is_ddr;
  prv_get_fast_read_params(dev, &instruction, &dummy_cycles, &is_ddr);
  qspi_mmap_start(dev->qspi, instruction, addr, dummy_cycles, size_bytes, is_ddr);

  // Point the buffer at the QSPI region
  uint32_t const volatile *const buffer = (uint32_t *)(QSPI_MMAP_BASE_ADDRESS + addr);
  uint32_t size_words = size_bytes / sizeof(uint32_t);
  for (uint32_t i = 0; i < size_words; ++i) {
    if (buffer[i] != FLASH_RESET_WORD_VALUE) {
      result = false;
      break;
    }
  }

  // stop memory mapped mode
  qspi_mmap_stop(dev->qspi);
  return result;
}
status_t qspi_flash_blank_check(QSPIFlash *dev, uint32_t addr, bool is_subsector) {
  qspi_use(dev->qspi);
#if TARGET_QEMU
  // QEMU doesn't support memory-mapping the FLASH
  const bool result = prv_blank_check_poll(dev, addr, is_subsector);
#else
  const bool result = prv_blank_check_mmap(dev, addr, is_subsector);
#endif
  qspi_release(dev->qspi);
  return result ? S_TRUE : S_FALSE;
}

void qspi_flash_ll_set_register_bits(QSPIFlash *dev, uint8_t read_instruction,
                                     uint8_t write_instruction, uint8_t value, uint8_t mask) {
  // make sure we're not trying to set any bits not within the mask
  PBL_ASSERTN((value & mask) == value);

  qspi_use(dev->qspi);

  // first read the register
  uint8_t reg_value;
  prv_read_register(dev, read_instruction, &reg_value, 1);

  // set the desired bits
  reg_value = (reg_value & ~mask) | value;

  // enable writing and write the register value
  prv_write_cmd_no_addr(dev, dev->state->part->instructions.write_enable);
  qspi_indirect_write_no_addr(dev->qspi, write_instruction, &reg_value, 1);

  qspi_release(dev->qspi);
}

static bool prv_protection_is_enabled(QSPIFlash *dev) {
  uint8_t status;
  prv_read_register(dev, dev->state->part->instructions.read_protection_status, &status, 1);
  return (status & dev->state->part->block_lock.protection_enabled_mask);
}

status_t qspi_flash_write_protection_enable(QSPIFlash *dev) {
#if TARGET_QEMU
  return S_NO_ACTION_REQUIRED;
#endif
  qspi_use(dev->qspi);
  prv_write_enable(dev);
  const bool already_enabled = prv_protection_is_enabled(dev);
  if (already_enabled == false) {
    PBL_LOG(LOG_LEVEL_INFO, "Enabling flash protection");
    // Enable write protection
    prv_write_cmd_no_addr(dev, dev->state->part->instructions.write_protection_enable);

    // Poll busy status until done
    qspi_poll_bit(dev->qspi, dev->state->part->instructions.rdsr1,
                  dev->state->part->status_bit_masks.busy, false /* !set */, QSPI_NO_TIMEOUT);
  }
  qspi_release(dev->qspi);

  return (already_enabled) ? S_NO_ACTION_REQUIRED : S_SUCCESS;
}

status_t qspi_flash_lock_sector(QSPIFlash *dev, uint32_t addr) {
#if TARGET_QEMU
  return S_SUCCESS;
#endif
  qspi_use(dev->qspi);

  prv_write_enable(dev);

  // Lock or unlock the sector
  const uint8_t instruction = dev->state->part->instructions.block_lock;
  if (dev->state->part->block_lock.has_lock_data) {
    qspi_indirect_write(dev->qspi, instruction, addr, &dev->state->part->block_lock.lock_data, 1);
  } else {
    qspi_indirect_write(dev->qspi, instruction, addr, NULL, 0);
  }

  // Poll busy status until done
  qspi_poll_bit(dev->qspi, dev->state->part->instructions.rdsr1,
                dev->state->part->status_bit_masks.busy, false /* !set */, QSPI_NO_TIMEOUT);

  // Read lock status
  uint8_t status;
  qspi_indirect_read(dev->qspi, dev->state->part->instructions.block_lock_status, addr, 0, &status,
                     sizeof(status), false);

  qspi_release(dev->qspi);

  return (status == dev->state->part->block_lock.locked_check) ? S_SUCCESS : E_ERROR;
}

status_t qspi_flash_unlock_all(QSPIFlash *dev) {
#if TARGET_QEMU
  return S_SUCCESS;
#endif
  qspi_use(dev->qspi);
  prv_write_enable(dev);
  prv_write_cmd_no_addr(dev, dev->state->part->instructions.block_unlock_all);
  qspi_release(dev->qspi);
  return S_SUCCESS;
}

#if !RELEASE
#include "console/prompt.h"
#include "drivers/flash.h"
#include "kernel/pbl_malloc.h"
#include "system/profiler.h"
#include "util/size.h"

static bool prv_flash_read_verify(QSPIFlash *dev, int size, int offset) {
  bool success = true;
  char *buffer_dma_ptr = kernel_malloc_check(size + offset + 3);
  char *buffer_pol = kernel_malloc_check(size + 3);
  char *buffer_mmap = kernel_malloc_check(size + 3);

  char *buffer_dma = buffer_dma_ptr + offset;

  // The buffers need to be different, so when compared against each other we can make
  // sure the write functions wrote the same thing.
  memset(buffer_dma, 0xA5, size);
  memset(buffer_pol, 0xCC, size);
  memset(buffer_mmap, 0x33, size);

  profiler_start();
  prv_read_mmap(dev, 0, buffer_mmap, size);
  profiler_stop();
  uint32_t mmap_time = profiler_get_total_duration(true);

  profiler_start();
  uint8_t instruction;
  uint8_t dummy_cycles;
  bool is_ddr;
  prv_get_fast_read_params(dev, &instruction, &dummy_cycles, &is_ddr);
  qspi_indirect_read_dma(dev->qspi, instruction, 0, dummy_cycles, buffer_dma, size, is_ddr);
  profiler_stop();
  uint32_t dma_time = profiler_get_total_duration(true);

  profiler_start();
  qspi_indirect_read(dev->qspi, instruction, 0, dummy_cycles, buffer_pol, size, is_ddr);
  profiler_stop();
  uint32_t pol_time = profiler_get_total_duration(true);

  if (memcmp(buffer_dma, buffer_pol, size) != 0) {
    prompt_send_response("FAILURE: buffer_dma != buffer_pol");
    success = false;
  }
  if (memcmp(buffer_dma, buffer_mmap, size) != 0) {
    prompt_send_response("FAILURE: buffer_dma != buffer_mmap");
    success = false;
  }

  const int buf_size = 64;
  char buf[buf_size];
  prompt_send_response_fmt(buf, buf_size,
                           "Size: %d DMA: %" PRIu32 " POL: %" PRIu32 " MMP: %" PRIu32, size,
                           dma_time, pol_time, mmap_time);

  kernel_free(buffer_dma_ptr);
  kernel_free(buffer_pol);
  kernel_free(buffer_mmap);

  return success;
}

struct FlashReadTestValues {
  int size;
  int offset;
};

const struct FlashReadTestValues FLASH_READ_TEST_TABLE[] = {
    {.size = 1024, .offset = 0}, {.size = 1025, .offset = 0}, {.size = 1026, .offset = 0},
    {.size = 1027, .offset = 0}, {.size = 1024, .offset = 1}, {.size = 1025, .offset = 2},
    {.size = 1026, .offset = 3}, {.size = 4, .offset = 0},    {.size = 20, .offset = 0},
    {.size = 60, .offset = 0},   {.size = 127, .offset = 0},  {.size = 128, .offset = 0},
};

void command_flash_apicheck(const char *len_str) {
  QSPIFlash *dev = QSPI_FLASH;
  const int buf_size = 64;
  char buf[buf_size];
  int failures = 0;
  int passes = 0;

  profiler_init();

  prompt_send_response("Check whoami");
  if (!qspi_flash_check_whoami(dev)) {
    ++failures;
    prompt_send_response("ERROR: Who am I failed");
  } else {
    ++passes;
  }

  prompt_send_response("Enter low power mode");
  flash_impl_enter_low_power_mode();

  // WHOAMI should fail in low-power mode
  prompt_send_response("Check whoami, should fail in low power mode");
  if (qspi_flash_check_whoami(dev)) {
    ++failures;
    prompt_send_response("ERROR: Who am I failed");
  } else {
    ++passes;
  }

  prompt_send_response("Exit low power mode");
  flash_impl_exit_low_power_mode();

  prompt_send_response("Start flash_read_verify test");
  qspi_use(dev->qspi);

  const int final_size = atoi(len_str);

  // If size is 0 run through a pre-defined table
  if (final_size == 0) {
    for (unsigned int i = 0; i < ARRAY_LENGTH(FLASH_READ_TEST_TABLE); ++i) {
      bool result = prv_flash_read_verify(dev, FLASH_READ_TEST_TABLE[i].size,
                                          FLASH_READ_TEST_TABLE[i].offset);
      if (!result) {
        ++failures;
      } else {
        ++passes;
      }
    }

  } else {
    if (prv_flash_read_verify(dev, final_size, 3)) {
      ++passes;
    } else {
      ++failures;
      prompt_send_response("ERROR: flash_read_verify failed");
    }
  }

  qspi_release(dev->qspi);

  bool was_busy = false;

  // write a few bytes to the sector we're going to erase so it's not empty
  uint8_t dummy_data = 0x55;
  flash_write_bytes(&dummy_data, FLASH_REGION_FIRMWARE_DEST_BEGIN, sizeof(dummy_data));
  profiler_start();
  status_t result = flash_impl_erase_sector_begin(FLASH_REGION_FIRMWARE_DEST_BEGIN);
  flash_impl_get_erase_status();
  if (result == S_SUCCESS) {
    while (flash_impl_get_erase_status() == E_BUSY) {
      was_busy = true;
    }
  }
  profiler_stop();
  uint32_t duration = profiler_get_total_duration(true);
  prompt_send_response_fmt(buf, buf_size, "Erase took: %" PRIu32, duration);

  // Fash erases take at least ~100ms, if we're too short we probably didn't erase
  const uint32_t min_erase_time = 10000;
  if (result != S_SUCCESS) {
    ++failures;
    prompt_send_response_fmt(buf, buf_size, "FAILURE: erase did not report success %" PRIi32,
                             result);
  } else if (was_busy == false) {
    ++failures;
    prompt_send_response("FAILURE: Flash never became busy, but we should be busy for 300ms.");
    prompt_send_response("FAILURE: Flash probably never did an erase.");
  } else if (duration < min_erase_time) {
    ++failures;
    prompt_send_response("FAILURE: Flash erase completed way to quickly to have succeeded.");
  } else {
    ++passes;
  }

  // must call blank_check_poll by hand, otherwise we'll get the dma version
  profiler_start();
  qspi_use(dev->qspi);
  bool is_blank =
      qspi_flash_blank_check(QSPI_FLASH, FLASH_REGION_FIRMWARE_DEST_BEGIN, SUBSECTOR_SIZE_BYTES);
  qspi_release(dev->qspi);
  profiler_stop();

  uint32_t blank = profiler_get_total_duration(true);
  prompt_send_response_fmt(buf, buf_size, "Sector blank check via read took: %" PRIu32, blank);
  if (is_blank != S_TRUE) {
    ++failures;
    prompt_send_response("FAILURE: sector not blank!?!");
  } else {
    ++passes;
  }

  profiler_start();
  is_blank = flash_impl_blank_check_subsector(FLASH_REGION_FIRMWARE_DEST_BEGIN);
  profiler_stop();

  blank = profiler_get_total_duration(true);
  prompt_send_response_fmt(buf, buf_size, "Subsector blank check via read took: %" PRIu32, blank);
  if (is_blank != S_TRUE) {
    ++failures;
    prompt_send_response("FAILURE: sector not blank!?!");
  } else {
    ++passes;
  }

  if (failures == 0) {
    prompt_send_response_fmt(buf, buf_size, "SUCCESS: run %d tests and all passeed", passes);
  } else {
    prompt_send_response_fmt(buf, buf_size, "FAILED: run %d tests and %d failed", passes + failures,
                             failures);
  }
}

#endif

#if RECOVERY_FW
#include "console/prompt.h"
#include "drivers/flash.h"

#define SIGNAL_TEST_MAGIC_PATTERN (0xA5)
#define TEST_BUFFER_SIZE (1024)
static uint8_t s_test_buffer[TEST_BUFFER_SIZE];
static const uint32_t s_test_addr = FLASH_REGION_FIRMWARE_DEST_END - SECTOR_SIZE_BYTES;
static bool s_signal_test_initialized;

void command_flash_signal_test_init(void) {
  // just test one sector, which is probably less than the size of the region

  // erase the sector
  flash_erase_sector_blocking(s_test_addr);

  // set the contents of the sector such that we will end up reading alternating 1s and 0s
  memset(s_test_buffer, SIGNAL_TEST_MAGIC_PATTERN, sizeof(s_test_buffer));
  flash_write_bytes(s_test_buffer, s_test_addr, sizeof(s_test_buffer));

  QSPIFlash *dev = QSPI_FLASH;
  // Ensure DDR is disabled for write check
  prv_set_fast_read_ddr_enabled(dev, false);
  uint8_t instruction;
  uint8_t dummy_cycles;
  bool is_ddr;
  prv_get_fast_read_params(dev, &instruction, &dummy_cycles, &is_ddr);
  PBL_ASSERTN(!is_ddr);

  qspi_use(dev->qspi);
  qspi_indirect_read(dev->qspi, instruction, s_test_addr, dummy_cycles, s_test_buffer,
                     sizeof(s_test_buffer), is_ddr);

  prv_set_fast_read_ddr_enabled(dev, dev->default_fast_read_ddr_enabled);
  qspi_release(dev->qspi);

  bool success = true;
  for (uint32_t i = 0; i < sizeof(s_test_buffer); ++i) {
    if (s_test_buffer[i] != SIGNAL_TEST_MAGIC_PATTERN) {
      success = false;
      break;
    }
  }

  if (success) {
    prompt_send_response("Done!");
    s_signal_test_initialized = true;
  } else {
    prompt_send_response("ERROR: Data read (SDR mode) did not match data written!");
  }
}

void command_flash_signal_test_run(void) {
  if (!s_signal_test_initialized) {
    prompt_send_response("ERROR: 'flash signal test init' must be run first!");
    return;
  }

  QSPIFlash *dev = QSPI_FLASH;
  qspi_use(dev->qspi);

  // set to DDR
  prv_set_fast_read_ddr_enabled(dev, true);

  // issue the read
  uint8_t instruction;
  uint8_t dummy_cycles;
  bool is_ddr;
  prv_get_fast_read_params(dev, &instruction, &dummy_cycles, &is_ddr);
  PBL_ASSERTN(is_ddr);
  qspi_indirect_read(dev->qspi, instruction, s_test_addr, dummy_cycles, s_test_buffer,
                     sizeof(s_test_buffer), is_ddr);

  bool success = true;
  for (uint32_t i = 0; i < sizeof(s_test_buffer); ++i) {
    if (s_test_buffer[i] != SIGNAL_TEST_MAGIC_PATTERN) {
      success = false;
      break;
    }
  }

  // set back to default mode
  prv_set_fast_read_ddr_enabled(dev, dev->default_fast_read_ddr_enabled);
  qspi_release(dev->qspi);

  if (success) {
    prompt_send_response("Ok");
  } else {
    prompt_send_response("ERROR: Read value didn't match!");
  }
}
#endif
