#include <SPI.h>
#include <SPIMemory.h>

#define FLASH_CS_PIN 4
SPIFlash flash(FLASH_CS_PIN);

void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
  digitalWrite(LED_BUILTIN, LOW);
  
  Serial.begin(115200);
  while (!Serial) delay(10);
  
  delay(4000);  // Allow boot garbage to clear
  digitalWrite(LED_BUILTIN, HIGH);

  flash.setClock(5000000);  // Cap SPI clock speed at 5 MHz for stability
  //flash.error(true);        // Enable error reporting for troubleshooting

  delay(500); // Allow time to adjust

  if (!flash.begin()) {
    digitalWrite(LED_BUILTIN, LOW);
    Serial.write("ABORTED_DUMP_FAILURE"); // Mark end of failed dump
    // Stop if flash initialization fails and blink error sequence
    blinkError();
  }

  sendFlashInformation(); // Send flash info before the dump
  delay(4000);  // Wait before starting the dump
  Serial.write("START_OF_DUMP\n"); // Mark start of dump

  dumpFlashContents();

  // If dumpFlashContents exits, something went wrong and we should blink error sequence
  blinkError();
}

void loop() {
  // Do nothing
}

void sendFlashInformation() {
  Serial.write("START_OF_INFORM\n");
  Serial.print("JEDEC ID: 0x");
  Serial.println(flash.getJEDECID(), HEX);
  Serial.print("Capacity: ");
  Serial.println(flash.getCapacity());
  Serial.print("Max Page: ");
  Serial.println(flash.getMaxPage());
  Serial.write("END_OF_INFORM\n");
}

void dumpFlashContents() {
  const size_t blockSize = 256;
  uint8_t bufferA[blockSize]; // Two buffers, for reading each block twice
  uint8_t bufferB[blockSize];
  uint32_t address = 0;

  while (address < flash.getCapacity()) {
    bool success = false;

    // Retry reading the block until both reads match or max retries reached
    for (int retries = 0; retries < 5; retries++) {
      if (flash.readByteArray(address, bufferA, blockSize) && flash.readByteArray(address, bufferB, blockSize)) {
            
        // Compare the two buffers
        if (memcmp(bufferA, bufferB, blockSize) == 0) {
          success = true;
          break; // Matching reads, exit retry loop
        }
      }
    }

    if (success) {
      Serial.write(bufferA, blockSize);
    } else {
      // If unable to get consistent reads, indicate failure and stop
      Serial.write("READ_ERROR_AT_ADDRESS\n");
      Serial.print(address);
      break;
    }
    address += blockSize;
    delay(10);
  }

  Serial.write("END_OF_DUMP"); // Mark end of dump
  
  // Blink builtin led forever (until reset) to indicate finished dumping
  blinkSuccess();
}

void blinkError() {
  // Forever (until reset) blink builtin led in the error sequence
  while (true) {
    digitalWrite(LED_BUILTIN, LOW);
    delay(400);
    digitalWrite(LED_BUILTIN, HIGH);
    delay(400);
  }
}

void blinkSuccess() {
  // Forever (until reset) blink builtin led in the success sequence
  while (true) {
    digitalWrite(LED_BUILTIN, LOW);
    delay(800);
    digitalWrite(LED_BUILTIN, HIGH);
    delay(1500);
  }
}
