#include "diskii.h"

#ifdef TEENSYDUINO
#include <Arduino.h>
#else
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#endif

#include "applemmu.h" // for FLOATING

#include "globals.h"
#include "appleui.h"

#include "diskii-rom.h"

#define DISKIIMAGIC 0xAA

// how many CPU cycles do we wait to spin down the disk drive?
#define SPINDOWNDELAY (1023)

DiskII::DiskII(AppleMMU *mmu)
{
  this->mmu = mmu;

  curPhase[0] = curPhase[1] = 0;
  curHalfTrack[0] = curHalfTrack[1] = 0;
  curWozTrack[0] = curWozTrack[1] = 0xFF;

  writeMode = false;
  writeProt = false; // FIXME: expose an interface to this
  readWriteLatch = 0x00;
  sequencer = 0;
  dataRegister = 0;
  lastDiskRead[0] = lastDiskRead[1] = 0;

  disk[0] = disk[1] = NULL;
  diskIsSpinningUntil[0] = diskIsSpinningUntil[1] = 0;
  selectedDisk = 0;
}

DiskII::~DiskII()
{
}

bool DiskII::Serialize(int8_t fd)
{
  return false;

  // FIXME: all the new variables are missing ***

  g_filemanager->writeByte(fd, DISKIIMAGIC);

  g_filemanager->writeByte(fd, curHalfTrack[0]);
  g_filemanager->writeByte(fd, curHalfTrack[1]);

  g_filemanager->writeByte(fd, curPhase[0]);
  g_filemanager->writeByte(fd, curPhase[1]);

  g_filemanager->writeByte(fd, readWriteLatch);
  g_filemanager->writeByte(fd, writeMode);
  g_filemanager->writeByte(fd, writeProt);

  for (int i=0; i<2; i++) {
    if (disk[i]) {
      g_filemanager->writeByte(fd, 1);
    } else {
      g_filemanager->writeByte(fd, 0);
    }
    if (!disk[i]->Serialize(fd))
      return false;
  }

  g_filemanager->writeByte(fd, 
			   (diskIsSpinningUntil[0] & 0xFF000000) >> 24);
  g_filemanager->writeByte(fd, 
			   (diskIsSpinningUntil[0] & 0x00FF0000) >> 16);
  g_filemanager->writeByte(fd, 
			   (diskIsSpinningUntil[0] & 0x0000FF00) >> 8);
  g_filemanager->writeByte(fd, 
			   (diskIsSpinningUntil[0] & 0x000000FF)     );

  g_filemanager->writeByte(fd, 
			   (diskIsSpinningUntil[1] & 0xFF000000) >> 24);
  g_filemanager->writeByte(fd, 
			   (diskIsSpinningUntil[1] & 0x00FF0000) >> 16);
  g_filemanager->writeByte(fd, 
			   (diskIsSpinningUntil[1] & 0x0000FF00) >> 8);
  g_filemanager->writeByte(fd, 
			   (diskIsSpinningUntil[1] & 0x000000FF)     );
  

  g_filemanager->writeByte(fd, selectedDisk);

  g_filemanager->writeByte(fd, DISKIIMAGIC);

  return true;
}

bool DiskII::Deserialize(int8_t fd)
{
  return false;
  // FIXME: all the new variables are missing ***

  if (g_filemanager->readByte(fd) != DISKIIMAGIC) {
    return false;
  }

  curHalfTrack[0] = g_filemanager->readByte(fd);
  curHalfTrack[1] = g_filemanager->readByte(fd);

  curPhase[0] = g_filemanager->readByte(fd);
  curPhase[1] = g_filemanager->readByte(fd);

  readWriteLatch = g_filemanager->readByte(fd);
  writeMode = g_filemanager->readByte(fd);
  writeProt = g_filemanager->readByte(fd);

  for (int i=0; i<2; i++) {
    if (disk[i])
      delete disk[i];
    if (g_filemanager->readByte(fd) == 1) {
      disk[i] = new WozSerializer();
      if (!disk[i]->Deserialize(fd))
	return false;
    } else {
      disk[i] = NULL;
    }
  }

  diskIsSpinningUntil[0] = g_filemanager->readByte(fd);
  diskIsSpinningUntil[0] <<= 8;diskIsSpinningUntil[0] = g_filemanager->readByte(fd);
  diskIsSpinningUntil[0] <<= 8;diskIsSpinningUntil[0] = g_filemanager->readByte(fd);
  diskIsSpinningUntil[0] <<= 8;diskIsSpinningUntil[0] = g_filemanager->readByte(fd);

  diskIsSpinningUntil[1] = g_filemanager->readByte(fd);
  diskIsSpinningUntil[1] <<= 8;diskIsSpinningUntil[1] = g_filemanager->readByte(fd);
  diskIsSpinningUntil[1] <<= 8;diskIsSpinningUntil[1] = g_filemanager->readByte(fd);
  diskIsSpinningUntil[1] <<= 8;diskIsSpinningUntil[1] = g_filemanager->readByte(fd);

  selectedDisk = g_filemanager->readByte(fd);

  if (g_filemanager->readByte(fd) != DISKIIMAGIC) {
    return false;
  }

  return true;
}

void DiskII::Reset()
{
  curPhase[0] = curPhase[1] = 0;
  curHalfTrack[0] = curHalfTrack[1] = 0;

  writeMode = false;
  writeProt = false; // FIXME: expose an interface to this
  readWriteLatch = 0x00;

  ejectDisk(0);
  ejectDisk(1);
}

uint8_t DiskII::readSwitches(uint8_t s)
{
  switch (s) {
  case 0x00: // change stepper motor phase
    break;
  case 0x01:
    setPhase(0);
    break;
  case 0x02:
    break;
  case 0x03:
    setPhase(1);
    break;
  case 0x04:
    break;
  case 0x05:
    setPhase(2);
    break;
  case 0x06: // 3 off
    break;
  case 0x07: // 3 on
    setPhase(3);
    break;

  case 0x08: // drive off
    diskIsSpinningUntil[selectedDisk] = g_cpu->cycles + SPINDOWNDELAY; // 1 second lag
    if (diskIsSpinningUntil[selectedDisk] == -1 ||
	diskIsSpinningUntil[selectedDisk] == 0)
      diskIsSpinningUntil[selectedDisk] = 2; // fudge magic numbers; 0 is "off" and -1 is "forever".
    break;
  case 0x09: // drive on
    diskIsSpinningUntil[selectedDisk] = -1; // magic "forever"
    g_ui->drawOnOffUIElement(UIeDisk1_activity + selectedDisk, true); // FIXME: delay a bit? Queue for later drawing? ***

    // Start the given disk drive spinning
    lastDiskRead[selectedDisk] = g_cpu->cycles;
    break;

  case 0x0A: // select drive 1
    select(0);
    break;
  case 0x0B: // select drive 2
    select(1);
    break;

  case 0x0C: // shift one read or write byte
    readWriteLatch = readOrWriteByte();
    if (readWriteLatch & 0x80)
      sequencer = 0;
    break;

  case 0x0D: // load data register (latch)
    // This is complex and incomplete. cf. Logic State Sequencer, 
    // UTA2E, p. 9-14
    if (!writeMode) {
      if (isWriteProtected())
	readWriteLatch |= 0x80;
      else
	readWriteLatch &= 0x7F;
    }
    break;

  case 0x0E: // set read mode
    setWriteMode(false);
    break;
  case 0x0F: // set write mode
    setWriteMode(true);
    break;
  }

  // Any even address read returns the readWriteLatch (UTA2E Table 9.1,
  // p. 9-12, note 2)
  return (s & 1) ? FLOATING : readWriteLatch;
}

void DiskII::writeSwitches(uint8_t s, uint8_t v)
{
  switch (s) {
  case 0x00: // change stepper motor phase
    break;
  case 0x01:
    setPhase(0);
    break;
  case 0x02:
    break;
  case 0x03:
    setPhase(1);
    break;
  case 0x04:
    break;
  case 0x05:
    setPhase(2);
    break;
  case 0x06: // 3 off
    break;
  case 0x07: // 3 on
    setPhase(3);
    break;

  case 0x08: // drive off
    diskIsSpinningUntil[selectedDisk] = g_cpu->cycles + SPINDOWNDELAY; // 1 second lag
    if (diskIsSpinningUntil[selectedDisk] == -1 ||
	diskIsSpinningUntil[selectedDisk] == 0)
      diskIsSpinningUntil[selectedDisk] = 2; // fudge magic numbers; 0 is "off" and -1 is "forever".
    break;
  case 0x09: // drive on
    diskIsSpinningUntil[selectedDisk] = -1; // magic "forever"
    g_ui->drawOnOffUIElement(UIeDisk1_activity + selectedDisk, true); // FIXME: delay a bit? Queue for later drawing? ***

    // Start the given disk drive spinning
    lastDiskRead[selectedDisk] = g_cpu->cycles;
    break;

  case 0x0A: // select drive 1
    select(0);
    break;
  case 0x0B: // select drive 2
    select(1);
    break;

  case 0x0C: // shift one read or write byte
    if (readOrWriteByte() & 0x80)
      sequencer = 0;
    break;

  case 0x0D: // drive write
    // FIXME
    break;

  case 0x0E: // set read mode
    setWriteMode(false);
    break;

  case 0x0F: // set write mode
    setWriteMode(true);
    break;
  }

  // All writes update the latch
  if (writeMode) {
    readWriteLatch = v;
  }
}

/* The Disk ][ has a stepper motor that moves the head across the tracks.
 * Switches 0-7 turn off and on the four different magnet phases; pulsing 
 * from (e.g.) phase 0 to phase 1 makes the motor move up a track, and 
 * (e.g.) phase 1 to phase 0 makes the motor move down a track.
 *
 * Except that's not quite true: the stepper actually moves the head a 
 * _half_ track.
 *
 * This is a very simplified version of the stepper motor code. In theory, 
 * we should keep track of all 4 phase magnets; and then only move up or down
 * a half track when two adjacent motors are on (not three adjacent motors;
 * and not two opposite motors). But that physical characteristic isn't 
 * important for most diskettes, and our image formats aren't likely to 
 * be able to provide appropriate half-track data to the programs that played 
 * tricks with these half-tracks (for copy protection or whatever).
 * 
 * This setPhase is only called when turning *on* a phase. It's assumed that 
 * something is turning *off* the phases correctly; and that the combination 
 * of the previous phase that was on and the current phase that's being turned
 * on are reliable enough to determine direction.
 *
 * The _phase_delta array is four sets of offsets - one for each
 * current phase, detailing what the step will be given the next
 * phase.  This kind of emulates the messiness of going from phase 0
 * to 2 -- it's going to move forward two half-steps -- but then doing
 * the same thing again is just going to move you back two half-steps...
 *
 */

void DiskII::setPhase(uint8_t phase)
{
  const int8_t _phase_delta[16] = {  0,  1,  2, -1, // prev phase 0 -> 0/1/2/3
				    -1,  0,  1,  2, // prev phase 1 -> 0/1/2/3
				    -2, -1,  0,  1, // prev phase 2 -> 0/1/2/3
				     1, -2, -1,  0  // prev phase 3 -> 0/1/2/3
  };

  int8_t prevPhase = curPhase[selectedDisk];
  int8_t prevHalfTrack = curHalfTrack[selectedDisk];


  curHalfTrack[selectedDisk] += _phase_delta[(prevPhase * 4) + phase];
  curPhase[selectedDisk] = phase;

  // Cap at 35 tracks (a normal disk size). Some drives let you go farther, 
  // and we could support that by increasing this limit - but the images 
  // would be different too, so there would be more work to abstract out...
  if (curHalfTrack[selectedDisk] > 35 * 2 - 1) {
    curHalfTrack[selectedDisk] = 35 * 2 - 1;
  }

  // Don't go past the innermost track, of course.
  if (curHalfTrack[selectedDisk] < 0) {
    curHalfTrack[selectedDisk] = 0;
    // recalibrate! This is where the fun noise goes DaDaDaDaDaDaDaDaDa
  }

  if (curHalfTrack[selectedDisk] != prevHalfTrack) {
    // We're changing track - flush the old track back to disk

    curWozTrack[selectedDisk] = disk[selectedDisk]->trackNumberForQuarterTrack(curHalfTrack[selectedDisk]*2);
  }
}

bool DiskII::isWriteProtected()
{
  return (writeProt ? 0xFF : 0x00);
}

void DiskII::setWriteMode(bool enable)
{
  writeMode = enable;
}

static uint8_t _lc(char c)
{
  if (c >= 'A' && c <= 'Z') {
    c = c - 'A' + 'a';
  }
  return c;
}

static bool _endsWithI(const char *s1, const char *s2)
{
  if (strlen(s2) > strlen(s1)) {
    return false;
  }
  
  const char *p = &s1[strlen(s1)-1];
  int16_t l = strlen(s2)-1;
  while (l >= 0) {
    if (_lc(*p--) != _lc(s2[l]))
      return false;
    l--;
  }
  return true;
}

void DiskII::insertDisk(int8_t driveNum, const char *filename, bool drawIt)
{
  ejectDisk(driveNum);

  disk[driveNum] = new WozSerializer();
  disk[driveNum]->readFile(filename, false, T_AUTO); // FIXME error checking

  curWozTrack[driveNum] = disk[driveNum]->trackNumberForQuarterTrack(curHalfTrack[driveNum]*2);

  if (drawIt)
    g_ui->drawOnOffUIElement(UIeDisk1_state + driveNum, false);
}

void DiskII::ejectDisk(int8_t driveNum)
{
  if (disk[driveNum]) {
    delete disk[driveNum];
    disk[driveNum] = NULL;
    g_ui->drawOnOffUIElement(UIeDisk1_state + driveNum, true);
  }
}

void DiskII::select(int8_t which)
{
  if (which != 0 && which != 1)
    return;

  if (which != selectedDisk) {
#if 0
    *** fixme check if the drive is still "on"
    indicatorIsOn[selectedDisk] = 100; // spindown time (fixme)
    g_ui->drawOnOffUIElement(UIeDisk1_activity + selectedDisk, false); // FIXME: queue for later drawing?
#endif

    // set the selected disk drive
    selectedDisk = which;
  }

  // Update the current woz track for the given disk drive
  curWozTrack[selectedDisk] =
    disk[selectedDisk]->trackNumberForQuarterTrack(curHalfTrack[selectedDisk]*2);
}

uint8_t DiskII::readOrWriteByte()
{
  if (!disk[selectedDisk]) {
    //printf("reading from uninserted disk\n");
    return 0xFF;
  }

  // FIXME: not handling writes at all at the moment ***
  if (writeMode && !writeProt) {
    return 0;
  }

  uint32_t curCycles = g_cpu->cycles;
  bool updateCycles = false;

  if (diskIsSpinningUntil[selectedDisk] >= curCycles) {

    if (lastDiskRead[selectedDisk] == 0) {
      // assume it's a first-read-after-spinup; return the first valid data
      sequencer = disk[selectedDisk]->nextDiskBit(curWozTrack[selectedDisk]);
      updateCycles = true;
      goto done;
    }
    
    // Otherwise we figure out how many cycles we missed since the last
    // disk read, and pop the right number of bits off the woz track
    uint32_t missedCycles;
    missedCycles = curCycles - lastDiskRead[selectedDisk];
    
    missedCycles >>= 2;
    if (missedCycles)
      updateCycles = true;
    while (missedCycles) {
      sequencer <<= 1;
      sequencer |= disk[selectedDisk]->nextDiskBit(curWozTrack[selectedDisk]);
      missedCycles--;
    }
  }

    
 done:
  if (updateCycles) {
    // We only update the lastDiskRead counter if the number of passed
    // cycles indicates that we did some sort of work...
    lastDiskRead[selectedDisk] = curCycles;
  }
  
  return sequencer;
}

const char *DiskII::DiskName(int8_t num)
{
  // ***

  return "";
}

void DiskII::loadROM(uint8_t *toWhere)
{
#ifdef TEENSYDUINO
  Serial.println("loading DiskII rom");
  for (uint16_t i=0; i<=0xFF; i++) {
    toWhere[i] = pgm_read_byte(&romData[i]);
  }
#else
  printf("loading DiskII rom\n");
  memcpy(toWhere, romData, 256);
#endif
}

void DiskII::flushTrack(int8_t track, int8_t sel)
{
  // safety check: if we're write-protected, then how did we get here?
  if (writeProt) {
    g_display->debugMsg("DII: Write Protected");
    return;
  }

  // ***
}

void DiskII::maintenance(uint32_t cycle)
{
  // Handle spin-down for the drive. Drives stay on for a second after
  // the stop was noticed.
  for (int i=0; i<2; i++) {
    if (diskIsSpinningUntil[i] && 
	g_cpu->cycles > diskIsSpinningUntil[i]) {
      // Stop the given disk drive spinning
      lastDiskRead[i] = 0; // FIXME: magic value. We need a tristate for this. ***
      diskIsSpinningUntil[i] = 0;
      g_ui->drawOnOffUIElement(UIeDisk1_activity + i, false); // FIXME: queue for later drawing?
    }
  }
}
