//******************************************
// PC Engine & TurboGrafx dump code by tamanegi_taro
// Revision 1.0.1 April 18th 2018
//
// Special thanks
// sanni - Arduino cart reader
// skaman - ROM size detection
// NO-INTRO - CRC list for game name detection
//
//******************************************

/******************************************
  Defines
 *****************************************/
#define HUCARD 0
#define TURBOCHIP 1

#define DETECTION_SIZE 64
#define CHKSUM_SKIP 0
#define CHKSUM_OK 1
#define CHKSUM_ERROR 2

/******************************************
   Prototype Declarations
 *****************************************/
/* Hoping that sanni will use this progressbar function */
void draw_progressbar(uint32_t processedsize, uint32_t totalsize);
void pcsMenu(void);
void pceMenu(void);

/* Several PCE dedicated functions */
void pin_read_write_PCE(void);
void pin_init_PCE(void);
void setup_cart_PCE(void);
void reset_cart_PCE(void);
uint8_t read_byte_PCE(uint32_t address);
uint8_t write_byte_PCE(uint32_t address, uint8_t data);
uint32_t detect_rom_size_PCE(void);
void read_bank_PCE(uint32_t address_start, uint32_t address_end, uint32_t *processed_size, uint32_t total_size);
void read_rom_PCE(void);

/******************************************
   Variables
 *****************************************/
uint8_t pce_internal_mode; //0 - HuCARD, 1 - TurboChip

/******************************************
  Menu
*****************************************/
// PCE start menu
static const char pceMenuItem1[] PROGMEM = "HuCARD";
static const char pceMenuItem2[] PROGMEM = "Turbochip";
static const char* const menuOptionspce[] PROGMEM = {pceMenuItem1, pceMenuItem2};

// PCE card menu items
static const char pceCartMenuItem1[] PROGMEM = "Read Rom";
static const char pceCartMenuItem2[] PROGMEM = "Reset";
static const char* const menuOptionspceCart[] PROGMEM = {pceCartMenuItem1, pceCartMenuItem2};

void draw_progressbar(uint32_t processedsize, uint32_t totalsize)
{
  uint8_t currentstatus, i;
  static uint8_t previousstatus;

  //Find progressbar length and draw if processed size is not 0
  if (processedsize != 0)
  {

    // Progress bar
    if (processedsize >= totalsize)
    {
      //if processed size is equal to total process size, finish drawing progress bar
      currentstatus = 20;
    }
    else
    {
      //if processed size did not reach total process size, find how many "*" should be drawn
      currentstatus = processedsize / (totalsize / 20);
    }

    //Draw "*" if needed
    if (currentstatus > previousstatus)
    {
      for (i = previousstatus; i < currentstatus; i++)
      {
        if (i == 19)
        {
          //If end of progress bar, finish progress bar by drawing "]"
          print_Msg(F("]"));
        }
        else
        {
          print_Msg(F("*"));
        }
        //Update display
        display_Update();
      }
      //update previous "*" status
      previousstatus = currentstatus;
    }
  }
  else
  {
    //If processed size is 0, initialize and draw "["
    previousstatus = 0;
    print_Msg(F("["));
    display_Update();
  }
}

// PCE start menu
void pcsMenu(void) {
  // create menu with title and 3 options to choose from
  unsigned char pceDev;
  // Copy menuOptions out of progmem
  convertPgm(menuOptionspce, 2);
  pceDev = question_box("Select device", menuOptions, 2, 0);

  // wait for user choice to come back from the question box menu
  switch (pceDev)
  {
    case 0:
      //Hucard
      display_Clear();
      display_Update();
      pce_internal_mode = HUCARD;
      setup_cart_PCE();
      mode = mode_PCE;
      break;

    case 1:
      //Turbografx
      display_Clear();
      display_Update();
      pce_internal_mode = TURBOCHIP;
      setup_cart_PCE();
      mode = mode_PCE;
      break;
  }
}

void pin_read_write_PCE(void)
{
  // Set Address Pins to Output
  //A0-A7
  DDRF = 0xFF;
  //A8-A15
  DDRK = 0xFF;
  //A16-A19
  DDRL = (DDRL & 0xF0) | 0x0F;

  //Set Control Pin to Output CS(PL4)
  DDRL |= (1 << 4);

  //Set CS(PL4) to HIGH
  PORTL |= (1 << 4);

  // Set Control Pins to Output RST(PH0) RD(PH3) WR(PH5)
  DDRH |= (1 << 0) | (1 << 3) | (1 << 5);
  // Switch all of above to HIGH
  PORTH |= (1 << 0) | (1 << 3) | (1 << 5);

  // Set IRQ(PH4) to Input
  DDRH &= ~(1 << 4);
  // Activate Internal Pullup Resistors
  PORTH |= (1 << 4);

  // Set Data Pins (D0-D7) to Input
  DDRC = 0x00;

  // Enable Internal Pullups
  PORTC = 0xFF;

  reset_cart_PCE();
}

void pin_init_PCE(void)
{

  //Set Address Pins to input and pull up
  DDRF = 0x00;
  PORTF = 0xFF;
  DDRK = 0x00;
  PORTK = 0xFF;
  DDRL = 0x00;
  PORTL = 0xFF;
  DDRH &= ~((1 << 0) | (1 << 3) | (1 << 5) | (1 << 6));
  PORTH = (1 << 0) | (1 << 3) | (1 << 5) | (1 << 6);

  // Set IRQ(PH4) to Input
  DDRH &= ~(1 << 4);
  // Activate Internal Pullup Resistors
  PORTH |= (1 << 4);

  // Set Data Pins (D0-D7) to Input
  DDRC = 0x00;
  // Enable Internal Pullups
  PORTC = 0xFF;

}

void setup_cart_PCE(void)
{
  // Set cicrstPin(PG1) to Output
  DDRG |= (1 << 1);
  // Output a high to disable CIC
  PORTG |= (1 << 1);

  pin_init_PCE();

}

void reset_cart_PCE(void)
{
  //Set RESET as Low
  PORTH &= ~(1 << 0);
  delay(200);
  //Set RESET as High
  PORTH |= (1 << 0);
  delay(200);

}

void set_address_PCE(uint32_t address)
{
  //Set address
  PORTF = address & 0xFF;
  PORTK = (address >> 8) & 0xFF;
  PORTL = (PORTL & 0xF0) | ((address >> 16) & 0x0F);
}

uint8_t read_byte_PCE(uint32_t address)
{
  uint8_t ret;
  uint8_t address_byte;

  set_address_PCE(address);

  // Arduino running at 16Mhz -> one nop = 62.5ns -> 1000ns total
  __asm__("nop\n\t""nop\n\t""nop\n\t""nop\n\t""nop\n\t""nop\n\t""nop\n\t""nop\n\t""nop\n\t""nop\n\t""nop\n\t""nop\n\t""nop\n\t""nop\n\t""nop\n\t""nop\n\t");

  // Set CS(PL4) and RD(PH3) as LOW
  PORTL &= ~(1 << 4);
  PORTH &= ~(1 << 3);

  // Arduino running at 16Mhz -> one nop = 62.5ns -> 1000ns total
  __asm__("nop\n\t""nop\n\t""nop\n\t""nop\n\t""nop\n\t""nop\n\t""nop\n\t""nop\n\t""nop\n\t""nop\n\t""nop\n\t""nop\n\t""nop\n\t""nop\n\t""nop\n\t""nop\n\t");

  //read byte
  ret =  PINC;

  //Swap bit order for PC Engine HuCARD
  if (pce_internal_mode == HUCARD)
  {
    ret = ((ret & 0x01) << 7) | ((ret & 0x02) << 5) | ((ret & 0x04) << 3) | ((ret & 0x08) << 1) | ((ret & 0x10) >> 1) | ((ret & 0x20) >> 3) | ((ret & 0x40) >> 5) | ((ret & 0x80) >> 7);
  }

  // Set CS(PL4) and RD(PH3) as HIGH
  PORTL |= (1 << 4);
  PORTH |= (1 << 3);

  //return read data
  return ret;

}

uint8_t write_byte_PCE(uint32_t address, uint8_t data)
{
  uint8_t ret;
  uint8_t address_byte;

  set_address_PCE(address);

  // Arduino running at 16Mhz -> one nop = 62.5ns -> 1000ns total
  __asm__("nop\n\t""nop\n\t""nop\n\t""nop\n\t""nop\n\t""nop\n\t""nop\n\t""nop\n\t""nop\n\t""nop\n\t""nop\n\t""nop\n\t""nop\n\t""nop\n\t""nop\n\t""nop\n\t");

  //Swap bit order for PC Engine HuCARD
  if (pce_internal_mode == HUCARD)
  {
    data = ((data & 0x01) << 7) | ((data & 0x02) << 5) | ((data & 0x04) << 3) | ((data & 0x08) << 1) | ((data & 0x10) >> 1) | ((data & 0x20) >> 3) | ((data & 0x40) >> 5) | ((data & 0x80) >> 7);
  }

  //write byte
  PORTC = data;

  // Set Data Pins (D0-D7) to Output
  DDRC = 0xFF;

  // Set CS(PL4) and WR(PH5) as LOW
  PORTL &= ~(1 << 4);
  PORTH &= ~(1 << 5);

  // Arduino running at 16Mhz -> one nop = 62.5ns -> 1000ns total
  __asm__("nop\n\t""nop\n\t""nop\n\t""nop\n\t""nop\n\t""nop\n\t""nop\n\t""nop\n\t""nop\n\t""nop\n\t""nop\n\t""nop\n\t""nop\n\t""nop\n\t""nop\n\t""nop\n\t");

  // Set CS(PL4) and WR(PH5) as HIGH
  PORTL |= (1 << 4);
  PORTH |= (1 << 5);

  // Set Data Pins (D0-D7) to Input
  DDRC = 0x00;

  // Enable Internal Pullups
  PORTC = 0xFF;

  //return read data
  return ret;

}

//Confirm the size of ROM - 128Kb, 256Kb, 384Kb, 512Kb, 768Kb or 1024Kb
uint32_t detect_rom_size_PCE(void)
{
  uint32_t rom_size;
  uint8_t read_byte;
  uint8_t current_byte;
  uint8_t detect_128, detect_256, detect_512, detect_768;

  //Initialize variables
  detect_128 = 0;
  detect_256 = 0;
  detect_512 = 0;
  detect_768 = 0;

  //Set pins to read PC Engine cart
  pin_read_write_PCE();

  //Confirm where mirror address start from(128KB, 256KB, 512KB, 768, or 1024KB)
  for (current_byte = 0; current_byte < DETECTION_SIZE; current_byte++) {
    if ((current_byte != detect_128) && (current_byte != detect_256) && (current_byte != detect_512) && (current_byte != detect_768))
    {
      //If none matched, it is 1024KB
      break;
    }

    //read byte for 128KB, 256KB, 512KB detection
    read_byte = read_byte_PCE(current_byte);

    //128KB detection
    if (current_byte == detect_128)
    {
      if (read_byte_PCE(current_byte + 128UL * 1024UL) == read_byte)
      {
        detect_128++;
      }
    }

    //256KB detection
    if (current_byte == detect_256)
    {
      if (read_byte_PCE(current_byte + 256UL * 1024UL) == read_byte)
      {
        detect_256++;
      }
    }

    //512KB detection
    if (current_byte == detect_512)
    {
      if (read_byte_PCE(current_byte + 512UL * 1024UL) == read_byte)
      {
        detect_512++;
      }
    }

    //768KB detection
    read_byte = read_byte_PCE(current_byte + 512UL * 1024UL);
    if (current_byte == detect_768)
    {
      if (read_byte_PCE(current_byte + 768UL * 1024UL) == read_byte)
      {
        detect_768++;
      }
    }
  }

  //debug
  //sprintf(fileName, "%d %d %d %d", detect_128, detect_256, detect_512, detect_768); //using filename global variable as string. Initialzed in below anyways.
  //println_Msg(fileName);

  //ROM size detection by result
  if (detect_128 == DETECTION_SIZE)
  {
    rom_size = 128;
  }
  else if (detect_256 == DETECTION_SIZE)
  {
    if (detect_512 == DETECTION_SIZE)
    {
      rom_size = 256;
    }
    else
    {
      //Another confirmation for 384KB because 384KB hucard has data in 0x0--0x40000 and 0x80000--0xA0000(0x40000 is mirror of 0x00000)
      rom_size = 384;
    }
  }
  else if (detect_512 == DETECTION_SIZE)
  {
    rom_size = 512;
  }
  else if (detect_768 == DETECTION_SIZE)
  {
    rom_size = 768;
  }
  else
  {
    rom_size = 1024;
  }

  //If rom size is more than or equal to 512KB, detect Street fighter II'
  if (rom_size >= 512)
  {
    //Look for "NEC HE "
    if (read_byte_PCE(0x7FFF9) == 'N' && read_byte_PCE(0x7FFFA) == 'E'  && read_byte_PCE(0x7FFFB) == 'C'
        && read_byte_PCE(0x7FFFC) == ' ' && read_byte_PCE(0x7FFFD) == 'H' && read_byte_PCE(0x7FFFE) == 'E')
    {
      rom_size = 2560;
    }
  }

  return rom_size;
}

/* Must be address_start and address_end should be 512 byte aligned */
void read_bank_PCE(uint32_t address_start, uint32_t address_end, uint32_t *processed_size, uint32_t total_size)
{
  uint32_t currByte;
  uint16_t c;

  for (currByte = address_start; currByte < address_end; currByte += 512) {
    for (c = 0; c < 512; c++) {
      sdBuffer[c] = read_byte_PCE(currByte + c);
    }
    myFile.write(sdBuffer, 512);
    *processed_size += 512;
    draw_progressbar(*processed_size, total_size);
  }
}

//Get line from file and convert upper case to lower case
void skip_line(SdFile* readfile)
{
  int i = 0;
  char str_buf;

  while (readfile->available())
  {
    //Read 1 byte from file
    str_buf = readfile->read();

    //if end of file or newline found, execute command
    if (str_buf == '\r')
    {
      readfile->read(); //dispose \n because \r\n
      break;
    }
    i++;
  }//End while
}

//Get line from file and convert upper case to lower case
void get_line(char* str_buf, SdFile* readfile, uint8_t maxi)
{
  int i = 0;

  while (readfile->available())
  {
    //If line size is more than maximum array, limit it.
    if (i >= maxi)
    {
      i = maxi - 1;
    }

    //Read 1 byte from file
    str_buf[i] = readfile->read();

    //if end of file or newline found, execute command
    if (str_buf[i] == '\r')
    {
      str_buf[i] = '\0';
      readfile->read(); //dispose \n because \r\n
      break;
    }
    i++;
  }//End while
}

uint32_t calculate_crc32(int n, unsigned char c[], uint32_t r)
{
  int i, j;

  for (i = 0; i < n; i++) {
    r ^= c[i];
    for (j = 0; j < 8; j++)
      if (r & 1) r = (r >> 1) ^ 0xEDB88320UL;
      else       r >>= 1;
  }
  return r;
}

void crc_search(char *file_p, char *folder_p, uint32_t rom_size)
{
  SdFile rom, script;
  uint32_t r, crc, processedsize;
  char gamename[100];
  char crc_file[9], crc_search[9];
  uint8_t flag;
  flag = CHKSUM_SKIP;

  //Open list file. If no list file found, just skip
  sd.chdir("/"); //Set read directry to root
  if (script.open("PCE_CRC_LIST.txt", O_READ))
  {
    //Calculate CRC of ROM file
    sd.chdir(folder_p);
    if (rom.open(file_p, O_READ))
    {
      //Initialize flag as error
      flag = CHKSUM_ERROR;
      crc = 0xFFFFFFFFUL; //Initialize CRC
      display_Clear();
      println_Msg("Calculating chksum...");
      processedsize = 0;
      draw_progressbar(0, rom_size * 1024UL); //Initialize progress bar

      while (rom.available())
      {
        r = rom.read(sdBuffer, 512);
        crc = calculate_crc32(r, sdBuffer, crc);
        processedsize += r;
        draw_progressbar(processedsize, rom_size * 1024UL);
      }

      crc = crc ^ 0xFFFFFFFFUL; //Finish CRC calculation and progress bar
      draw_progressbar(rom_size * 1024UL, rom_size * 1024UL);

      //Display calculated CRC
      sprintf(crc_file, "%08lX", crc);

      //Search for same CRC in list
      while (script.available()) {
        //Read 2 lines (game name and CRC)
        get_line(gamename, &script, 96);
        get_line(crc_search, &script, 9);
        skip_line(&script); //Skip every 3rd line

        //if checksum search successful, rename the file and end search
        if (strcmp(crc_search, crc_file) == 0)
        {
          print_Msg("Chksum OK ");
          println_Msg(crc_file);
          print_Msg(F("Saved to "));
          print_Msg(folder_p);
          print_Msg(F("/"));
          print_Msg(gamename);
          print_Msg(F(".pce"));
          flag = CHKSUM_OK;
          strcat(gamename, ".pce");
          rom.rename(sd.vwd(), gamename);
          break;
        }
      }
      rom.close();
    }
  }


  if (flag == CHKSUM_SKIP)
  {
    print_Msg(F("Saved to "));
    print_Msg(folder_p);
    print_Msg(F("/"));
    print_Msg(file_p);
  }
  else if (flag == CHKSUM_ERROR)
  {
    print_Msg("Chksum Error ");
    println_Msg(crc_file);
    print_Msg(F("Saved to "));
    print_Msg(folder_p);
    print_Msg(F("/"));
    print_Msg(file_p);
  }

  script.close();

}


void read_rom_PCE(void)
{
  uint32_t rom_size;
  uint32_t processed_size = 0;

  //clear the screen
  display_Clear();
  rom_size = detect_rom_size_PCE();
  sprintf(fileName, "Detected size: %dKB", rom_size); //using filename global variable as string. Initialzed in below anyways.
  println_Msg(fileName);

  //debug
  //return;

  // Get name, add extension and convert to char array for sd lib
  strcpy(fileName, "PCEROM");
  strcat(fileName, ".pce");

  // create a new folder for the save file
  EEPROM_readAnything(10, foldern);
  sprintf(folder, "PCE/ROM/%s/%d", romName, foldern);
  sd.mkdir(folder, true);
  sd.chdir(folder);

  println_Msg(F("Saving ROM..."));
  display_Update();

  // write new folder number back to eeprom
  foldern = foldern + 1;
  EEPROM_writeAnything(10, foldern);

  //open file on sd card
  if (!myFile.open(fileName, O_RDWR | O_CREAT)) {
    print_Error(F("Can't create file on SD"), true);
  }

  pin_read_write_PCE();

  //Initialize progress bar by setting processed size as 0
  draw_progressbar(0, rom_size * 1024UL);

  if (rom_size == 384)
  {
    //Read two sections. 0x000000--0x040000 and 0x080000--0x0A0000 for 384KB
    read_bank_PCE(0, 0x40000, &processed_size, rom_size * 1024UL);
    read_bank_PCE(0x80000, 0xA0000, &processed_size, rom_size * 1024UL);
  }
  else if (rom_size == 2560)
  {
    //Dump Street fighter II' Champion Edition
    read_bank_PCE(0, 0x80000, &processed_size, rom_size * 1024UL);  //Read first bank
    write_byte_PCE(0x1FF0, 0xFF); //Display second bank
    read_bank_PCE(0x80000, 0x100000, &processed_size, rom_size * 1024UL); //Read second bank
    write_byte_PCE(0x1FF1, 0xFF); //Display third bank
    read_bank_PCE(0x80000, 0x100000, &processed_size, rom_size * 1024UL); //Read third bank
    write_byte_PCE(0x1FF2, 0xFF); //Display forth bank
    read_bank_PCE(0x80000, 0x100000, &processed_size, rom_size * 1024UL); //Read forth bank
    write_byte_PCE(0x1FF3, 0xFF); //Display fifth bank
    read_bank_PCE(0x80000, 0x100000, &processed_size, rom_size * 1024UL); //Read fifth bank
  }
  else
  {
    //Read start form 0x000000 and keep reading until end of ROM
    read_bank_PCE(0, rom_size * 1024UL, &processed_size, rom_size * 1024UL);
  }

  pin_init_PCE();

  //Close the file:
  myFile.close();

  //CRC search and rename ROM
  crc_search(fileName, folder, rom_size);

}




// SNES Menu
void pceMenu() {
  // create menu with title and 7 options to choose from
  unsigned char mainMenu;
  // Copy menuOptions out of progmem
  convertPgm(menuOptionspceCart, 2);

  if (pce_internal_mode == HUCARD)
  {
    mainMenu = question_box("PCE HuCARD menu", menuOptions, 2, 0);
  }
  else
  {
    mainMenu = question_box("TG TurboChip menu", menuOptions, 2, 0);
  }

  // wait for user choice to come back from the question box menu
  switch (mainMenu)
  {
    case 0:
      display_Clear();
      // Change working dir to root
      sd.chdir("/");
      read_rom_PCE();
      break;

    case 1:
      asm volatile ("  jmp 0");
      break;
  }
  println_Msg(F(""));
  println_Msg(F("Press Button..."));
  display_Update();
  wait();
}


//******************************************
// End of File
//******************************************