//============================================================================
//
//   SSSS    tt          lll  lll
//  SS  SS   tt           ll   ll
//  SS     tttttt  eeee   ll   ll   aaaa
//   SSSS    tt   ee  ee  ll   ll      aa
//      SS   tt   eeeeee  ll   ll   aaaaa  --  "An Atari 2600 VCS Emulator"
//  SS  SS   tt   ee      ll   ll  aa  aa
//   SSSS     ttt  eeeee llll llll  aaaaa
//
// Copyright (c) 1995-2018 by Bradford W. Mott, Stephen Anthony
// and the Stella Team
//
// See the file "License.txt" for information on usage and redistribution of
// this file, and for a DISCLAIMER OF ALL WARRANTIES.
//============================================================================

#include "OSystem.hxx"
#include "Serializer.hxx"
#include "System.hxx"
#include "CartCTYTunes.hxx"
#include "CartCTY.hxx"

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
CartridgeCTY::CartridgeCTY(const BytePtr& image, uInt32 size,
                           const OSystem& osystem)
  : Cartridge(osystem.settings()),
    myOSystem(osystem),
    myOperationType(0),
    myCounter(0),
    myLDAimmediate(false),
    myRandomNumber(0x2B435044),
    myRamAccessTimeout(0),
    myAudioCycles(0),
    myFractionalClocks(0.0),
    myBankOffset(0)
{
  // Copy the ROM image into my buffer
  memcpy(myImage, image.get(), std::min(32768u, size));
  createCodeAccessBase(32768);

  // Point to the first tune
  myFrequencyImage = CartCTYTunes;

  // Remember startup bank (not bank 0, since that's ARM code)
  myStartBank = 1;
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void CartridgeCTY::reset()
{
  initializeRAM(myRAM, 64);

  myRAM[0] = myRAM[1] = myRAM[2] = myRAM[3] = 0xFF;

  myAudioCycles = 0;
  myFractionalClocks = 0.0;

  // Upon reset we switch to the startup bank
  bank(myStartBank);
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void CartridgeCTY::install(System& system)
{
  mySystem = &system;

  // Map all RAM accesses to call peek and poke
  System::PageAccess access(this, System::PA_READ);
  for(uInt16 addr = 0x1000; addr < 0x1080; addr += System::PAGE_SIZE)
    mySystem->setPageAccess(addr, access);

  // Install pages for the startup bank
  bank(myStartBank);
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
uInt8 CartridgeCTY::peek(uInt16 address)
{
  uInt16 peekAddress = address;
  address &= 0x0FFF;
  uInt8 peekValue = myImage[myBankOffset + address];

  // In debugger/bank-locked mode, we ignore all hotspots and in general
  // anything that can change the internal state of the cart
  if(bankLocked())
    return peekValue;

  // Check for aliasing to 'LDA #$F2'
  if(myLDAimmediate && peekValue == 0xF2)
  {
    myLDAimmediate = false;

    // Update the music data fetchers (counter & flag)
    updateMusicModeDataFetchers();

#if 0
    // using myDisplayImage[] instead of myProgramImage[] because waveforms
    // can be modified during runtime.
    uInt32 i = myDisplayImage[(myMusicWaveforms[0] << 5) + (myMusicCounters[0] >> 27)] +
               myDisplayImage[(myMusicWaveforms[1] << 5) + (myMusicCounters[1] >> 27)] +
               myDisplayImage[(myMusicWaveforms[2] << 5) + (myMusicCounters[2] >> 27)];
    return = (uInt8)i;
#endif
    return 0xF2;  // FIXME - return frequency value here
  }
  else
    myLDAimmediate = false;

  if(address < 0x0040)  // Write port is at $1000 - $103F (64 bytes)
  {
    // Reading from the write port triggers an unwanted write
    uInt8 value = mySystem->getDataBusState(0xFF);

    if(bankLocked())
      return value;
    else
    {
      triggerReadFromWritePort(peekAddress);
      return myRAM[address] = value;
    }
  }
  else if(address < 0x0080)  // Read port is at $1040 - $107F (64 bytes)
  {
    address -= 0x40;
    switch(address)
    {
      case 0x00:  // Error code after operation
        return myRAM[0];
      case 0x01:  // Get next Random Number (8-bit LFSR)
        myRandomNumber = ((myRandomNumber & (1<<10)) ? 0x10adab1e: 0x00) ^
                         ((myRandomNumber >> 11) | (myRandomNumber << 21));
        return myRandomNumber & 0xFF;
      case 0x02:  // Get Tune position (low byte)
        return myCounter & 0xFF;
      case 0x03:  // Get Tune position (high byte)
        return (myCounter >> 8) & 0xFF;
      default:
        return myRAM[address];
    }
  }
  else  // Check hotspots
  {
    switch(address)
    {
      case 0x0FF4:
        // Bank 0 is ARM code and not actually accessed
        return ramReadWrite();
      case 0x0FF5:
      case 0x0FF6:
      case 0x0FF7:
      case 0x0FF8:
      case 0x0FF9:
      case 0x0FFA:
      case 0x0FFB:
        // Banks 1 through 7
        bank(address - 0x0FF4);
        break;
      default:
        break;
    }

    // Is this instruction an immediate mode LDA?
    myLDAimmediate = (peekValue == 0xA9);

    return peekValue;
  }
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
bool CartridgeCTY::poke(uInt16 address, uInt8 value)
{
  address &= 0x0FFF;

//cerr << "POKE: address=" << HEX4 << address << ", value=" << HEX2 << value << endl;
  if(address < 0x0040)  // Write port is at $1000 - $103F (64 bytes)
  {
    switch(address)  // FIXME for functionality
    {
      case 0x00:  // Operation type for $1FF4
        myOperationType = value;
        break;
      case 0x01:  // Set Random seed value (reset)
        myRandomNumber = 0x2B435044;
        break;
      case 0x02:  // Reset fetcher to beginning of tune
        myCounter = 0;
        break;
      case 0x03:  // Advance fetcher to next tune position
        myCounter = (myCounter + 3) & 0x0fff;
        break;
      default:
        myRAM[address] = value;
        break;
    }
  }
  else  // Check hotspots
  {
    switch(address)
    {
      case 0x0FF4:
        // Bank 0 is ARM code and not actually accessed
        ramReadWrite();
        break;
      case 0x0FF5:
      case 0x0FF6:
      case 0x0FF7:
      case 0x0FF8:
      case 0x0FF9:
      case 0x0FFA:
      case 0x0FFB:
        // Banks 1 through 7
        bank(address - 0x0FF4);
        break;
      default:
        break;
    }
  }
  return false;
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
bool CartridgeCTY::bank(uInt16 bank)
{
  if(bankLocked()) return false;

  // Remember what bank we're in
  myBankOffset = bank << 12;

  // Setup the page access methods for the current bank
  System::PageAccess access(this, System::PA_READ);
  for(uInt16 addr = 0x1080; addr < 0x2000; addr += System::PAGE_SIZE)
  {
    access.codeAccessBase = &myCodeAccessBase[myBankOffset + (addr & 0x0FFF)];
    mySystem->setPageAccess(addr, access);
  }
  return myBankChanged = true;
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
uInt16 CartridgeCTY::getBank() const
{
  return myBankOffset >> 12;
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
uInt16 CartridgeCTY::bankCount() const
{
  return 8;
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
bool CartridgeCTY::patch(uInt16 address, uInt8 value)
{
  address &= 0x0FFF;

  if(address < 0x0080)
  {
    // Normally, a write to the read port won't do anything
    // However, the patch command is special in that ignores such
    // cart restrictions
    myRAM[address & 0x003F] = value;
  }
  else
    myImage[myBankOffset + address] = value;

  return myBankChanged = true;
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
const uInt8* CartridgeCTY::getImage(uInt32& size) const
{
  size = 32768;
  return myImage;
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
bool CartridgeCTY::save(Serializer& out) const
{
  try
  {
    out.putString(name());
    out.putShort(getBank());
    out.putByteArray(myRAM, 64);

    out.putByte(myOperationType);
    out.putShort(myCounter);
    out.putBool(myLDAimmediate);
    out.putInt(myRandomNumber);
    out.putLong(myAudioCycles);
    out.putDouble(myFractionalClocks);

  }
  catch(...)
  {
    cerr << "ERROR: CartridgeCTY::save" << endl;
    return false;
  }

  return true;
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
bool CartridgeCTY::load(Serializer& in)
{
  try
  {
    if(in.getString() != name())
      return false;

    // Remember what bank we were in
    bank(in.getShort());
    in.getByteArray(myRAM, 64);

    myOperationType = in.getByte();
    myCounter = in.getShort();
    myLDAimmediate = in.getBool();
    myRandomNumber = in.getInt();
    myAudioCycles = in.getLong();
    myFractionalClocks = in.getDouble();
  }
  catch(...)
  {
    cerr << "ERROR: CartridgeCTY::load" << endl;
    return false;
  }
  return true;
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void CartridgeCTY::setRomName(const string& name)
{
  myEEPROMFile = myOSystem.nvramDir() + name + "_eeprom.dat";
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
uInt8 CartridgeCTY::ramReadWrite()
{
  /* The following algorithm implements accessing Harmony cart EEPROM

    1. Wait for an access to hotspot location $1FF4 (return 1 in bit 6
       while busy).

    2. Determine operation from myOperationType.

    3. Save or load relevant EEPROM memory to/from a file.

    4. Set byte 0 of RAM+ memory to zero to indicate success (will
       always happen in emulation).

    5. Return 0 (in bit 6) on the next access to $1FF4, if enough time has
       passed to complete the operation on a real system (0.5 s for read,
       1 s for write).
  */

  if(bankLocked()) return 0xff;

  // First access sets the timer
  if(myRamAccessTimeout == 0)
  {
    // Opcode and value in form of XXXXYYYY (from myOperationType), where:
    //    XXXX = index and YYYY = operation
    uInt8 index = myOperationType >> 4;
    switch(myOperationType & 0xf)
    {
      case 1:  // Load tune (index = tune)
        if(index < 7)
        {
          // Add 0.5 s delay for read
          myRamAccessTimeout = myOSystem.getTicks() + 500000;
          loadTune(index);
        }
        break;
      case 2:  // Load score table (index = table)
        if(index < 4)
        {
          // Add 0.5 s delay for read
          myRamAccessTimeout = myOSystem.getTicks() + 500000;
          loadScore(index);
        }
        break;
      case 3:  // Save score table (index = table)
        if(index < 4)
        {
          // Add 1 s delay for write
          myRamAccessTimeout = myOSystem.getTicks() + 1000000;
          saveScore(index);
        }
        break;
      case 4:  // Wipe all score tables
        // Add 1 s delay for write
        myRamAccessTimeout = myOSystem.getTicks() + 1000000;
        wipeAllScores();
        break;
    }
    // Bit 6 is 1, busy
    return myImage[myBankOffset + 0xFF4] | 0x40;
  }
  else
  {
    // Have we reached the timeout value yet?
    if(myOSystem.getTicks() >= myRamAccessTimeout)
    {
      myRamAccessTimeout = 0;  // Turn off timer
      myRAM[0] = 0;            // Successful operation

      // Bit 6 is 0, ready/success
      return myImage[myBankOffset + 0xFF4] & ~0x40;
    }
    else
      // Bit 6 is 1, busy
      return myImage[myBankOffset + 0xFF4] | 0x40;
  }
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void CartridgeCTY::loadTune(uInt8 index)
{
  // Each tune is offset by 4096 bytes
  // Instead of copying non-modifiable data around (as would happen on the
  // Harmony), we simply point to the appropriate tune
  myFrequencyImage = CartCTYTunes + (index << 12);

  // Reset to beginning of tune
  myCounter = 0;
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void CartridgeCTY::loadScore(uInt8 index)
{
  Serializer serializer(myEEPROMFile, true);
  if(serializer)
  {
    uInt8 scoreRAM[256];
    try
    {
      serializer.getByteArray(scoreRAM, 256);
    }
    catch(...)
    {
      memset(scoreRAM, 0, 256);
    }
    // Grab 60B slice @ given index (first 4 bytes are ignored)
    memcpy(myRAM+4, scoreRAM + (index << 6) + 4, 60);
  }
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void CartridgeCTY::saveScore(uInt8 index)
{
  Serializer serializer(myEEPROMFile);
  if(serializer)
  {
    // Load score RAM
    uInt8 scoreRAM[256];
    try
    {
      serializer.getByteArray(scoreRAM, 256);
    }
    catch(...)
    {
      memset(scoreRAM, 0, 256);
    }

    // Add 60B RAM to score table @ given index (first 4 bytes are ignored)
    memcpy(scoreRAM + (index << 6) + 4, myRAM+4, 60);

    // Save score RAM
    serializer.rewind();
    try
    {
      serializer.putByteArray(scoreRAM, 256);
    }
    catch(...)
    {
      // Maybe add logging here that save failed?
      cerr << name() << ": ERROR saving score table " << int(index) << endl;
    }
  }
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
void CartridgeCTY::wipeAllScores()
{
  Serializer serializer(myEEPROMFile);
  if(serializer)
  {
    // Erase score RAM
    uInt8 scoreRAM[256];
    memset(scoreRAM, 0, 256);
    try
    {
      serializer.putByteArray(scoreRAM, 256);
    }
    catch(...)
    {
      // Maybe add logging here that save failed?
      cerr << name() << ": ERROR wiping score tables" << endl;
    }
  }
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
inline void CartridgeCTY::updateMusicModeDataFetchers()
{
  // Calculate the number of cycles since the last update
  uInt32 cycles = uInt32(mySystem->cycles() - myAudioCycles);
  myAudioCycles = mySystem->cycles();

  // Calculate the number of CTY OSC clocks since the last update
  double clocks = ((20000.0 * cycles) / 1193191.66666667) + myFractionalClocks;
  uInt32 wholeClocks = uInt32(clocks);
  myFractionalClocks = clocks - double(wholeClocks);

  // Let's update counters and flags of the music mode data fetchers
  if(wholeClocks > 0)
    for(int x = 0; x <= 2; ++x)
      ; //myMusicCounters[x] += myMusicFrequencies[x] * wholeClocks;
}
