/* Mednafen - Multi-system Emulator
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

#include <mednafen/mednafen.h>
#include <mednafen/general.h>
#include <mednafen/mednafen-endian.h>
#include <mednafen/FileStream.h>

#include "CDAccess_CHD.h"

extern retro_log_printf_t log_cb;


// Disk-image(rip) track/sector formats
enum
{
   DI_FORMAT_AUDIO       = 0x00,
   DI_FORMAT_MODE1       = 0x01,
   DI_FORMAT_MODE1_RAW   = 0x02,
   DI_FORMAT_MODE2       = 0x03,
   DI_FORMAT_MODE2_FORM1 = 0x04,
   DI_FORMAT_MODE2_FORM2 = 0x05,
   DI_FORMAT_MODE2_RAW   = 0x06,
   _DI_FORMAT_COUNT
};

static const char *DI_CUE_Strings[7] = 
{
   "AUDIO",
   "MODE1/2048",
   "MODE1/2352",

   "MODE2/2336",
   "MODE2/2048",
   "MODE2/2324",
   "MODE2/2352"
};

bool CDAccess_CHD::ImageOpen(const char *path, bool image_memcache)
{
   chd_error err = chd_open(path, CHD_OPEN_READ, NULL, &chd);
   if (err != CHDERR_NONE)
      return false;

   if (image_memcache)
   {
      err = chd_precache(chd);
      if (err != CHDERR_NONE)
         return false;
   }

   /* allocate storage for sector reads */
   const chd_header *head = chd_get_header(chd);
   hunkmem = (uint8_t*)malloc(head->hunkbytes);
   oldhunk = -1;
   
   log_cb(RETRO_LOG_INFO, "chd_load '%s' hunkbytes=%d\n", path, head->hunkbytes);

   int plba = -150;
   uint32_t fileOffset = 0;
   //int rlba = 0;

   char type[64], subtype[32], pgtype[32], pgsub[32];

   char meta_entry[256];
   uint32_t meta_entry_size = 0;

   while (1)
   {
      int tkid = 0, frames = 0, pad = 0, pregap = 0, postgap = 0;

      err = chd_get_metadata(chd, CDROM_TRACK_METADATA2_TAG, NumTracks, meta_entry, sizeof(meta_entry), &meta_entry_size, NULL, NULL);
      if (err == CHDERR_NONE)
      {
         sscanf(meta_entry, CDROM_TRACK_METADATA2_FORMAT,
            &tkid, type, subtype, &frames, &pregap, pgtype, pgsub, &postgap);
      }
      else
      {
         err = chd_get_metadata(chd, CDROM_TRACK_METADATA_TAG, NumTracks, meta_entry, sizeof(meta_entry), &meta_entry_size, NULL, NULL);
         if (err == CHDERR_NONE)
         {
            sscanf(meta_entry, CDROM_TRACK_METADATA_FORMAT,
               &tkid, type, subtype, &frames);
         }
         else
         {
            /* if there's no valid metadata, this is the end of the TOC */
            break;
         }
      }

      if (strncmp(type, "MODE2_RAW", 9) != 0 && strncmp(type, "AUDIO", 5) != 0)
      {
         log_cb(RETRO_LOG_ERROR, "chd_parse track type %s unsupported\n", type);
         return false;
      }
      else if (strncmp(subtype, "NONE", 4) != 0)
      {
         log_cb(RETRO_LOG_ERROR, "chd_parse track subtype %s unsupported\n", subtype);
         return false;
      }

      /* add track */
      NumTracks++;

      if (NumTracks != tkid)
      {
         // should this be treated as a fatal error?
         log_cb(RETRO_LOG_WARN, "chd tracks are out of order, missing a track or contain a duplicate!\n");
      }

      if (strncmp(type, "MODE2_RAW", 9) == 0)
      {
         Tracks[tkid].DIFormat = DI_FORMAT_MODE2_RAW;
         Tracks[tkid].subq_control |= SUBQ_CTRLF_DATA;
      }
      else if (strncmp(type, "AUDIO", 5) == 0)
      {
         Tracks[tkid].DIFormat = DI_FORMAT_AUDIO;
         Tracks[tkid].subq_control &= ~SUBQ_CTRLF_DATA;
         Tracks[tkid].RawAudioMSBFirst = true;
      }

      Tracks[tkid].pregap = (tkid == 1) ? 150 : (pgtype[0] == 'V') ? 0 : pregap;
      Tracks[tkid].pregap_dv = (pgtype[0] == 'V') ? pregap : 0;
      plba += Tracks[tkid].pregap + Tracks[tkid].pregap_dv;
      Tracks[tkid].LBA = plba;
      Tracks[tkid].postgap = postgap;
      Tracks[tkid].sectors = frames - Tracks[tkid].pregap_dv;
      Tracks[tkid].SubchannelMode = 0;
      Tracks[tkid].index[0] = -1;
      Tracks[tkid].index[1] = 0;

      fileOffset += Tracks[tkid].pregap_dv;
      Tracks[tkid].FileOffset = fileOffset;
      fileOffset += frames - Tracks[tkid].pregap_dv;
      fileOffset += Tracks[tkid].postgap;
      fileOffset += ((frames + 3) & ~3) - frames;

      plba += frames - Tracks[tkid].pregap_dv;
      plba += Tracks[tkid].postgap;

      total_sectors += (tkid == 1) ? frames : frames + Tracks[tkid].pregap;

      if (tkid < FirstTrack)
         FirstTrack = tkid;
      if (tkid > LastTrack)
         LastTrack = tkid;

      /*Tracks[tkid].pregap = pregap;
      Tracks[tkid].postgap = postgap;
      Tracks[tkid].index[0] = rlba - pregap;
      Tracks[tkid].index[1] = rlba;
      Tracks[tkid].pregap_dv = Tracks[tkid].index[1] - Tracks[tkid].index[0];
      Tracks[tkid].LBA = rlba;
      Tracks[tkid].sectors = frames;

      Tracks[tkid].FileOffset = rlba * 2352;

      total_sectors += frames;

      if (tkid == 1)
         rlba += frames + 150;
      else
         rlba += frames;*/
   }

   // prepare sbi file path
   std::string base_dir, file_base, file_ext;
   char sbi_ext[4] = { 's', 'b', 'i', 0 };

   MDFN_GetFilePathComponents(path, &base_dir, &file_base, &file_ext);

   if(file_ext.length() == 4 && file_ext[0] == '.')
   {
      for(int i = 0; i < 3; i++)
      {
         if(file_ext[1 + i] >= 'A' && file_ext[1 + i] <= 'Z')
            sbi_ext[i] += 'A' - 'a';
      }
   }
   sbi_path = MDFN_EvalFIP(base_dir, file_base + std::string(".") + std::string(sbi_ext), true);

   return true;
}

void CDAccess_CHD::Cleanup(void)
{
   if(chd != NULL)
      chd_close(chd);

   if (hunkmem != NULL)
      free(hunkmem);
}

CDAccess_CHD::CDAccess_CHD(const char *path, bool image_memcache)
{
   chd = NULL;

   NumTracks = 0;
   total_sectors = 0;
   memset(Tracks, 0, sizeof(Tracks));

   // initialize opposites
   FirstTrack = 99;
   LastTrack = 0;

   if (!ImageOpen(path, image_memcache))
   {
   }
}

CDAccess_CHD::~CDAccess_CHD()
{
   Cleanup();
}

// Note: this function makes use of the current contents(as in |=) in SubPWBuf.
int32_t CDAccess_CHD::MakeSubPQ(int32 lba, uint8 *SubPWBuf)
{
   unsigned i;
   uint8_t buf[0xC], adr, control;
   int32_t track;
   uint32_t lba_relative;
   uint32_t ma, sa, fa;
   uint32_t m, s, f;
   uint8_t pause_or = 0x00;
   bool track_found = false;

   for(track = FirstTrack; track < (FirstTrack + NumTracks); track++)
   {
      if(lba >= (Tracks[track].LBA - Tracks[track].pregap_dv - Tracks[track].pregap) && lba < (Tracks[track].LBA + Tracks[track].sectors + Tracks[track].postgap))
      {
         track_found = true;
         break;
      }
   }


   if(!track_found)
      track = FirstTrack;

   lba_relative = abs((int32)lba - Tracks[track].LBA);

   f            = (lba_relative % 75);
   s            = ((lba_relative / 75) % 60);
   m            = (lba_relative / 75 / 60);

   fa           = (lba + 150) % 75;
   sa           = ((lba + 150) / 75) % 60;
   ma           = ((lba + 150) / 75 / 60);

   adr          = 0x1; // Q channel data encodes position
   control      = Tracks[track].subq_control;

   // Handle pause(D7 of interleaved subchannel byte) bit, should be set to 1 when in pregap or postgap.
   if((lba < Tracks[track].LBA) || (lba >= Tracks[track].LBA + Tracks[track].sectors))
      pause_or = 0x80;

   // Handle pregap between audio->data track
   {
      int32_t pg_offset = (int32)lba - Tracks[track].LBA;

      // If we're more than 2 seconds(150 sectors) from the real "start" of the track/INDEX 01, and the track is a data track,
      // and the preceding track is an audio track, encode it as audio(by taking the SubQ control field from the preceding track).
      //
      // TODO: Look into how we're supposed to handle subq control field in the four combinations of track types(data/audio).
      //
      if(pg_offset < -150)
      {
         if((Tracks[track].subq_control & SUBQ_CTRLF_DATA) && (FirstTrack < track) && !(Tracks[track - 1].subq_control & SUBQ_CTRLF_DATA))
            control = Tracks[track - 1].subq_control;
      }
   }

   memset(buf, 0, 0xC);
   buf[0] = (adr << 0) | (control << 4);
   buf[1] = U8_to_BCD(track);

   if(lba < Tracks[track].LBA) // Index is 00 in pregap
      buf[2] = U8_to_BCD(0x00);
   else
      buf[2] = U8_to_BCD(0x01);

   /* Track relative MSF address */
   buf[3] = U8_to_BCD(m);
   buf[4] = U8_to_BCD(s);
   buf[5] = U8_to_BCD(f);
   buf[6] = 0;
   /* Absolute MSF address */
   buf[7] = U8_to_BCD(ma);
   buf[8] = U8_to_BCD(sa);
   buf[9] = U8_to_BCD(fa);

   subq_generate_checksum(buf);

   if(!SubQReplaceMap.empty())
   {
      std::map<uint32, cpp11_array_doodad>::const_iterator it = SubQReplaceMap.find(LBA_to_ABA(lba));

      if(it != SubQReplaceMap.end())
         memcpy(buf, it->second.data, 12);
   }

   for (i = 0; i < 96; i++)
      SubPWBuf[i] |= (((buf[i >> 3] >> (7 - (i & 0x7))) & 1) ? 0x40 : 0x00) | pause_or;

   return track;
}

bool CDAccess_CHD::Read_Raw_Sector(uint8 *buf, int32 lba)
{
   uint8_t SimuQ[0xC];
   int32_t track;
   CDRFILE_TRACK_INFO *ct;

   //
   // Leadout synthesis
   //
   if (lba >= total_sectors)
   {
      uint8_t data_synth_mode = 0x01; // Default for DISC_TYPE_CDDA_OR_M1, would be 0x02 for DISC_TYPE_CD_XA

      switch (Tracks[LastTrack].DIFormat)
      {
         case DI_FORMAT_AUDIO:
            break;

         case DI_FORMAT_MODE1_RAW:
         case DI_FORMAT_MODE1:
            data_synth_mode = 0x01;
            break;

         case DI_FORMAT_MODE2_RAW:
         case DI_FORMAT_MODE2_FORM1:
         case DI_FORMAT_MODE2_FORM2:
         case DI_FORMAT_MODE2:
            data_synth_mode = 0x02;
            break;
      }
      synth_leadout_sector_lba(data_synth_mode, ptoc, lba, buf);
   }

   memset(buf + 2352, 0, 96);
   track = MakeSubPQ(lba, buf + 2352);
   subq_deinterleave(buf + 2352, SimuQ);

   ct = &Tracks[track];

   //
   // Handle pregap and postgap reading
   //
   if (lba < (ct->LBA - ct->pregap_dv) || lba >= (ct->LBA + ct->sectors))
   {
      int32_t pg_offset = lba - ct->LBA;
      CDRFILE_TRACK_INFO *et = ct;

      if (pg_offset < -150)
      {
         if ((Tracks[track].subq_control & SUBQ_CTRLF_DATA) && (FirstTrack < track) && !(Tracks[track - 1].subq_control & SUBQ_CTRLF_DATA))
         et = &Tracks[track - 1];
      }

      memset(buf, 0, 2352);
      switch (et->DIFormat)
      {
         case DI_FORMAT_AUDIO:
            break;

         case DI_FORMAT_MODE1_RAW:
         case DI_FORMAT_MODE1:
            encode_mode1_sector(lba + 150, buf);
            break;

         case DI_FORMAT_MODE2_RAW:
         case DI_FORMAT_MODE2_FORM1:
         case DI_FORMAT_MODE2_FORM2:
         case DI_FORMAT_MODE2:
            buf[12 + 6] = 0x20;
            buf[12 + 10] = 0x20;
            encode_mode2_form2_sector(lba + 150, buf);
            // TODO: Zero out optional(?) checksum bytes?
            break;
      }
   }
   else
   {
      // read CHD hunk
      const chd_header *head = chd_get_header(chd);
      //int cad = (((lba - ct->LBA) * 2352) + ct->FileOffset) / 2352;
      int cad = lba - ct->LBA + ct->FileOffset;
      int sph = head->hunkbytes / (2352 + 96);
      int hunknum = cad / sph; //(cad * head->unitbytes) / head->hunkbytes;
      int hunkofs = cad % sph; //(cad * head->unitbytes) % head->hunkbytes;
      int err = CHDERR_NONE;

      /* each hunk holds ~8 sectors, optimize when reading contiguous sectors */
      if (hunknum != oldhunk)
      {
         err = chd_read(chd, hunknum, hunkmem);
         if (err != CHDERR_NONE)
            log_cb(RETRO_LOG_ERROR, "chd_read_sector failed lba=%d error=%d\n", lba, err);
         else
            oldhunk = hunknum;
      }

      memcpy(buf, hunkmem + hunkofs * (2352 + 96), 2352);

      if (ct->DIFormat == DI_FORMAT_AUDIO && ct->RawAudioMSBFirst)
         Endian_A16_Swap(buf, 588 * 2);
   }
   return true;
}

bool CDAccess_CHD::Read_Raw_PW(uint8_t *buf, int32_t lba)
{
   memset(buf, 0, 96);
   MakeSubPQ(lba, buf);
   return true;
}

bool CDAccess_CHD::Read_TOC(TOC *toc)
{
   TOC_Clear(toc);

   toc->first_track = FirstTrack;
   toc->last_track = LastTrack;
   toc->disc_type = DISC_TYPE_CD_XA;   // always?

   // read track info
   for(int i = 1; i <= NumTracks; i++)
   {
      toc->tracks[i].control = Tracks[i].subq_control;
      toc->tracks[i].adr = ADR_CURPOS;
      toc->tracks[i].lba = Tracks[i].LBA;
   }

   toc->tracks[100].lba = total_sectors;
   toc->tracks[100].adr = ADR_CURPOS;
   toc->tracks[100].control = toc->tracks[toc->last_track].control & 0x4;

   // Convenience leadout track duplication.
   if (toc->last_track < 99)
      toc->tracks[toc->last_track + 1] = toc->tracks[100];

   if (!SubQReplaceMap.empty())
      SubQReplaceMap.clear();

   // Load SBI file, if present
   if (filestream_exists(sbi_path.c_str()))
      LoadSBI(sbi_path.c_str());

   ptoc = toc;
   log_cb(RETRO_LOG_INFO, "chd_read_toc: finished\n");
   return true;
}

int CDAccess_CHD::LoadSBI(const char* sbi_path)
{
   /* Loading SBI file */
   uint8 header[4];
   uint8 ed[4 + 10];
   uint8 tmpq[12];
   FileStream sbis(sbi_path, MODE_READ);

   sbis.read(header, 4);

   if(memcmp(header, "SBI\0", 4))
      return -1;

   while(sbis.read(ed, sizeof(ed)) == sizeof(ed))
   {
      /* Bad BCD MSF offset in SBI file. */
      if(!BCD_is_valid(ed[0]) || !BCD_is_valid(ed[1]) || !BCD_is_valid(ed[2]))
         return -1;

      /* Unrecognized boogly oogly in SBI file */
      if(ed[3] != 0x01)
         return -1;

      memcpy(tmpq, &ed[4], 10);

      subq_generate_checksum(tmpq);
      tmpq[10] ^= 0xFF;
      tmpq[11] ^= 0xFF;

      uint32 aba = AMSF_to_ABA(BCD_to_U8(ed[0]), BCD_to_U8(ed[1]), BCD_to_U8(ed[2]));

      memcpy(SubQReplaceMap[aba].data, tmpq, 12);
   }

   log_cb(RETRO_LOG_INFO, "[CHD] Loaded SBI file %s\n", sbi_path);
   return 0;
}

void CDAccess_CHD::Eject(bool eject_status)
{

}
