/* AXPbox Alpha Emulator
 * Copyright (C) 2020 Tomáš Glozar
 * Website: https://github.com/lenticularis39/axpbox
 *
 * Forked from: ES40 emulator
 * Copyright (C) 2007-2008 by the ES40 Emulator Project
 * Copyright (C) 2007 by Camiel Vanderhoeven
 *
 * 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., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
 * USA.
 *
 * Although this is not required, the author would appreciate being notified of,
 * and receiving any modifications you may make to the source code that might
 * serve the general public.
 */

/**
 * \file
 * Contains the code for the emulated Floppy Controller devices.
 **/
#include "FloppyController.hpp"
#include "AliM1543C.hpp"
#include "DMA.hpp"
#include "Disk.hpp"
#include "StdAfx.hpp"
#include "System.hpp"

/**
 * Constructor.
 **/
CFloppyController::CFloppyController(CConfigurator *cfg, CSystem *c, int id)
    : CSystemComponent(cfg, c), CDiskController(1, 2) {
  c->RegisterMemory(this, 1536, U64(0x00000801fc0003f0) - (0x80 * id), 6);
  c->RegisterMemory(this, 1537, U64(0x00000801fc0003f7) - (0x80 * id), 1);

  state.cmd_parms_ptr = 0;
  state.cmd_res_ptr = 0;
  state.status.rqm = 1;
  state.status.dio = 0;

  state.interrupt = false;
  state.dor = 0x0C;
  state.reset_sense_cnt = 0;

  printf("%s: $Id$\n", devid_string);
}

/**
 * Destructor.
 **/
CFloppyController::~CFloppyController() {}

std::string datarate_name[] = {"500 Kb/S MFM", "300 Kb/S MFM", "250 Kb/S MFM",
                               "1 Mb/S MFM"};

struct cmdinfo_t {
  u8 command;
  u8 parms;
  u8 returns;
  std::string name;
} cmdinfo[] = {
    {0, 0, 0, ""},
    {0, 0, 0, ""},
    {2, 9, 7, "Read Track"},
    {3, 3, 0, "Specify"},
    {4, 2, 1, "Sense Drive Status"},
    {5, 9, 7, "Write Data"},
    {6, 9, 7, "Read Data"},
    {7, 2, 0, "Recalibrate"},
    {8, 1, 2, "Sense Interrupt Status"},
    {9, 9, 7, "Write Deleted Data"},
    {10, 2, 7, "Read ID"},
    {11, 0, 0, ""},
    {12, 9, 7, "Read Deleted"},
    {13, 6, 7, "Format Track"},
    {14, 1, 10, "DumpReg"},
    {15, 3, 0, "Seek"},
    {16, 1, 1, "Version"},
    {17, 9, 7, "Scan Equal"},
    {18, 2, 0, "Perpendicular Mode"},
    {19, 4, 0, "Configure"},
    {20, 1, 1, "Lock"},
    {21, 0, 0, ""},
    {22, 9, 7, "Verify"},
    {23, 0, 0, ""},
    {24, 0, 0, ""},
    {25, 9, 7, "Scan Low or Equal"},
    {26, 0, 0, ""},
    {27, 0, 0, ""},
    {28, 0, 0, ""},
    {29, 9, 7, "Scan High or Equal"},
    {30, 0, 0, ""},
    {31, 0, 0, ""},
};

void CFloppyController::WriteMem(int index, u64 address, int dsize, u64 data) {
  if (index == 1537)
    address += 7;

  // printf("FDC: Write port %d, value: %x\n", address, data);

  switch (address) {
  case FDC_REG_STATUS_A:
  case FDC_REG_STATUS_B:
    printf("FDC: Read only register %" PRId64 " written.\n", address);
    break;

  case FDC_REG_DOR: {
    u8 old_dor = state.dor;
    state.dor = data;
    // bit 4 = drive 0 motor, bit 5 = drive 1 motor
    // bit 3 = dma enable (ps/2 reserved?)
    // bit 2 = 1: fdc enable (reset), 0: hold at reset
    // bits 1-0:  drive select 0: a, 1: b, I assume 2 & 3 are reserved.

    state.drive[0].motor = (data & 0x10) >> 4;
    state.drive[1].motor = (data & 0x20) >> 5;
    state.dma = (data & 0x08) >> 3;
    state.drive_select = data & 0x03;

    printf("FDC:  motor a: %s, motor b: %s, dma: %s, drive: %s\n",
           state.drive[0].motor ? "on" : "off",
           state.drive[1].motor ? "on" : "off", state.dma ? "on" : "off",
           state.drive_select == 0 ? "A" : "B");

    if ((data & 0x04) == 0) {
      state.cmd_parms_ptr = 0;
      state.cmd_res_ptr = 0;
      state.cmd_res_max = 0;
      state.status.rqm = 1;
      state.status.dio = 0;
      state.status.nondma = !state.dma;
      state.reset_sense_cnt = 0;
      state.drive[0].seeking = 0;
      state.drive[1].seeking = 0;
      clear_interrupt();
    } else if ((old_dor & 0x04) == 0) {
      state.reset_sense_cnt = 4;
      do_interrupt();
    }
    break;
  }
  case FDC_REG_TAPE:
    printf("FDC: Tape register written with %" PRIx64 "\n", data);
    break;

  case FDC_REG_STATUS: // write = data rate selector
    // bit 7 = software reset (self clearing)
    // bit 6 = power down
    // bit 5 = reserved (0)
    // bit 4-2 = write precomp (000 = default)
    // bit 1-0 = data rate select

    state.datarate = data & 0x03;
    state.write_precomp = (data & 0x1c) >> 2;
    printf("FDC: data rate %s, precomp: %d\n",
           datarate_name[state.datarate].c_str(), state.write_precomp);

    break;

  case FDC_REG_COMMAND:
    // the excitement happens here.
    if (state.status.dio) {
      printf("Unrequested data byte to command port.  Throwing away.\n");
      break;
    } else {
      state.cmd_parms[state.cmd_parms_ptr++] = data;
      int cmd = state.cmd_parms[0] & 0x1F;
      state.cmd_res_max = cmdinfo[cmd].returns;
      // printf("FDC: parm_ptr: %d, parms: %d\n", state.cmd_parms_ptr,
      // cmdinfo[cmd].parms);
      if (state.cmd_parms_ptr == cmdinfo[cmd].parms) {
        printf("FDC: command %s(", cmdinfo[cmd].name.c_str());
        for (int i = 1; i < state.cmd_parms_ptr; i++) {
          printf("%x ", state.cmd_parms[i]);
        }
        printf(")\n");

        state.cmd_res_max = cmdinfo[cmd].returns;
        state.cmd_res_ptr = 0;
        state.status.rqm = 0;
        switch (cmd) {
        case 3: // specify
          // set up some hardware parameters.  We really don't care about
          // the times (step rate time, head unload time, head load time}, but
          // we may care about the ND bit (parm byte 3, bit 1)...
          state.dma = ~(state.cmd_parms[2] & 0x01);
          break;

        case 4: // Sense Drive Status
        {
          int drive_idx = state.cmd_parms[1] & 3;
          int head = (state.cmd_parms[1] >> 2) & 1;
          u8 st3 = 0x08; // bit 3 = 1 (Ready)
          if (state.drive[drive_idx].cylinder == 0)
            st3 |= 0x10; // Track 0
          st3 |= (head << 2);
          st3 |= drive_idx;
          if (FDISK(drive_idx) && FDISK(drive_idx)->ro())
            st3 |= 0x40;
          state.cmd_res[0] = st3;
          break;
        }

        case 5: // write data
        case 6: // read data
                // args:
                // 0: bit 7 = MT (multitrack), 6 = MFM, 5 = SK (skip flag)
                // 1: bit 2 = HDS (head), 1 = DS1, 0 = DS0
                // 2: C = cyl
                // 3: H = head address
                // 4: R = sector
                // 5: N = sector size, 2 = 512b
                // 6: EOT = end of track 0x24 = 36 sectors (18 * 2)
                // 7: GPL = gap length
                // 8: DTL = sector size (if N = 0)
        {
          int drive_idx = state.cmd_parms[1] & 0x03;
          int head = (state.cmd_parms[1] >> 2) & 1;

          // The FDC chip is always present via es40-cfg generated configs,
          // but a R/W data command issued to a drive with not present or
          // no disk needs to term abnormally instead of dereferencing a null
          // ST0 IC=01 (abnormal termination) with the Not Ready bit set.
          if (FDISK(drive_idx) == NULL) {
            printf("FDC [CMD %02x]: drive %d not ready (no media) - aborting\n",
                   cmd, drive_idx);
            state.cmd_res[0] = 0x40 | ST0_NR | (head << 2) | drive_idx; // ST0
            state.cmd_res[1] = 0;                                       // ST1
            state.cmd_res[2] = 0;                                       // ST2
            state.cmd_res[3] = state.cmd_parms[2]; // C (cylinder)
            state.cmd_res[4] = state.cmd_parms[3]; // H (head)
            state.cmd_res[5] = state.cmd_parms[4]; // R (sector)
            state.cmd_res[6] = state.cmd_parms[5]; // N (sector size)
            do_interrupt();
            break; // -> switch(cmd) tail arms the result phase (rqm=1, dio=1)
          }

          int cyl = state.cmd_parms[2];
          int sector = state.cmd_parms[4];
          int eot = state.cmd_parms[6];
          if (eot == 0)
            eot = 18;
          int pos = (cyl * 2 + head) * 18 + sector - 1; // 1.44MB

          bool mt = (state.cmd_parms[0] & 0x80) ? true : false;
          int sectors_to_read = 0;
          if (mt && head == 0) {
            sectors_to_read = (eot - sector + 1) + eot;
          } else {
            sectors_to_read = eot - sector + 1;
          }
          if (sectors_to_read <= 0)
            sectors_to_read = 1;

          size_t fdc_count = sectors_to_read * 512;
          size_t dma_count = theDMA->get_count(2) + 1;
          size_t count = (fdc_count < dma_count) ? fdc_count : dma_count;

          printf("FDC [CMD %02x]: CHS=(%d/%d/%d) EOT=%d MT=%d. Drive=%d\n", cmd,
                 cyl, head, sector, eot, mt, drive_idx);
          printf(
              "FDC [DMA]: Transfer size requested = %zu bytes (%zu sectors)\n",
              count, count / 512);

          u8 *buffer = new u8[count];
          memset(buffer, 0, count);

          printf("FDC [LBA]: Calculated LBA = %d (offset 0x%x)\n", pos,
                 pos * 512);

          SEL_FDISK->seek_byte((off_t_large)pos * 512);
          if (cmd == 6) {
            SEL_FDISK->read_bytes(buffer, count);
            printf("FDC: read data:  %zx @ %x\n  ", count, pos * 512);
            for (int i = 0; i < count; i++) {
              printf("%02x ", *((char *)buffer + i) & 0xff);
              if (i % 16 == 15)
                printf("\n  ");
            }
            printf("\n");
            theDMA->send_data(2, buffer, count);
          } else {
            theDMA->recv_data(2, buffer, count);
            printf("FDC: write data:  %zx @ %x\n  ", count, pos * 512);
            for (int i = 0; i < count; i++) {
              printf("%02x ", *((char *)buffer + i) & 0xff);
              if (i % 16 == 15)
                printf("\n  ");
            }
            printf("\n");
            SEL_FDISK->write_bytes(buffer, count);
          }
          delete[] buffer;

          int sectors_read = count / 512;
          if (sectors_read == 0)
            sectors_read = 1;

          for (int i = 0; i < sectors_read; i++) {
            state.cmd_parms[4]++;
            if (state.cmd_parms[4] > eot) {
              state.cmd_parms[4] = 1;
              if (mt && state.cmd_parms[3] == 0) {
                state.cmd_parms[3] = 1;
              } else {
                state.cmd_parms[3] = 0;
                state.cmd_parms[2]++;
              }
            }
          }

          state.cmd_res[0] = drive_idx | (head << 2);
          state.cmd_res[1] = 0;
          state.cmd_res[2] = 0;
          state.cmd_res[3] = state.cmd_parms[2];
          state.cmd_res[4] = state.cmd_parms[3];
          state.cmd_res[5] = state.cmd_parms[4];
          state.cmd_res[6] = state.cmd_parms[5];
          state.drive[drive_idx].seeking = 1;

          do_interrupt();
        } break;

        case 7: // recalibrate
        {
          int drive_idx = state.cmd_parms[1] & 3;
          state.drive[drive_idx].seeking =
              3; // wait for 3 status reads to finish seek.
          state.drive[drive_idx].cylinder = 0;
          do_interrupt();
        } break;

        case 8: // sense interrupt status
          if (!state.interrupt) {
            state.cmd_res[0] = 0x80;
            state.cmd_res[1] = 0;
          } else {
            int drive_idx = state.drive_select & 3;
            state.cmd_res[0] = 0x20 | drive_idx; // Seek End
            clear_interrupt();
            state.cmd_res[1] =
                state.drive[drive_idx].cylinder; // present cylinder number
          }
          break;

        case 10: // Read ID
        {
          int drive_idx = state.cmd_parms[1] & 3;
          int head = (state.cmd_parms[1] >> 2) & 1;
          state.cmd_res[0] = drive_idx | (head << 2);
          state.cmd_res[1] = 0;
          state.cmd_res[2] = 0;
          state.cmd_res[3] = state.drive[drive_idx].cylinder;
          state.cmd_res[4] = head;
          state.cmd_res[5] = 1;
          state.cmd_res[6] = 2; // 512 bytes
          do_interrupt();
          break;
        }

        case 14: // DumpReg
          // we're software, we don't care (I think)
          break;

        case 15: // seek
          // args:
          // 0: opcode
          // 1: bit 2 = HDS (head), 1 = DS1, 0 = DS0
          // 2: NCN = new cylinder number
          SEL_DRIVE.seeking = 3; // wait 3 status reads to finish seek.
          SEL_DRIVE.cylinder = state.cmd_parms[2];
          do_interrupt();
          break;

        case 16:                   // Version
          state.cmd_res[0] = 0x90; // 82077 compatible
          break;

        case 18: // perpendicular mode
          // We really don't care, somehow
          break;

        case 19: // configure
          // we're software, we don't care (I think)
          break;

        case 20: // Lock
          state.cmd_res[0] =
              (state.cmd_parms[0] >> 3) & 0x10; // per the datasheet
          break;

        default:
          printf("Unhandled floppy command: %d = %s\n", cmd,
                 cmdinfo[cmd].name.c_str());
          exit(1);
        }

        state.status.rqm = 1;
        if (cmdinfo[cmd].returns > 0) {
          state.status.dio = 1;
        }
        state.cmd_parms_ptr = 0;
      } else {
        // printf("FDC: command parameter byte %d = %x, expecting %d bytes for
        // %s\n", state.cmd_parms_ptr-1, data, cmdinfo[state.cmd_parms[0] &
        // 0x1f].parms, cmdinfo[state.cmd_parms[0] &0x1f].name.c_str());
      }
    }

    break;

  case FDC_REG_DIR:
    // PC/AT, PS/2
    //    bits 7-2 = reserved
    //    bit 0-1 = MFM data rate
    state.datarate = data & 0x03;
    printf("FDC: data rate %s\n", datarate_name[state.datarate].c_str());

    break;
  }
}

u64 CFloppyController::ReadMem(int index, u64 address, int dsize) {
  u64 data = 0;

  if (index == 1537)
    address += 7;

  switch (address) {
  case FDC_REG_STATUS_A:
    // bit 7 = interrupt pending
    // bit 6 = -DRV2  (second drive installed)
    // bit 5 = step
    // bit 4 = -track0
    // bit 3 = head1 select
    // bit 2 = -index
    // bit 1 = -write protect
    // bit 0 = +direction

    break;

  case FDC_REG_STATUS_B:
    // bit 7-6 reserved (1)
    // bit 5 = drive select
    // bit 4 = write data
    // bit 3 = read data
    // bit 2 = write enable
    // bit 1 = motor 1 enable
    // bit 0 = motor 0 enable

    break;

  case FDC_REG_DOR:
  case FDC_REG_TAPE:
    printf("FDC: Write only register %" PRId64 " read.", address);
    break;

  case FDC_REG_STATUS:
    data = get_status();
    break;

  case FDC_REG_COMMAND:
    // The data comes back from here.
    data = state.cmd_res[state.cmd_res_ptr++];
    if (state.cmd_res_ptr >= state.cmd_res_max) {
      state.status.rqm = 1;
      state.status.dio = 0;
      state.cmd_res_ptr = 0;
      state.cmd_res_max = 0;
      clear_interrupt();
    }

    break;

  case FDC_REG_DIR:
    // PS/2 mode:
    //    bit 7 = diskette change
    //    bits 6-3 = 1
    //    bit 2 = datarate select 1
    //    bit 1 = datarate select 0
    //    bit 0 = high density select

    int drive_idx = state.drive_select & 3;
    if (drive_idx < 2 && FDISK(drive_idx) != NULL) {
      data = 0x00; // Disk present
    } else {
      data = 0x80; // No disk
    }
    break;
  }

  printf("FDC: Read register %" PRId64 ", value: %" PRIx64 "\n", address, data);

  return data;
}

static u32 fdc_magic1 = 0x0fdc0fdc;
static u32 fdc_magic2 = 0xfdc0fdc0;

int CFloppyController::SaveState(FILE *f) {
  long ss = sizeof(state);

  fwrite(&fdc_magic1, sizeof(u32), 1, f);
  fwrite(&ss, sizeof(long), 1, f);
  fwrite(&state, sizeof(state), 1, f);
  fwrite(&fdc_magic2, sizeof(u32), 1, f);
  printf("fdc: %ld bytes saved.\n", ss);
  return 0;
}

int CFloppyController::RestoreState(FILE *f) {
  long ss;
  u32 m1;
  u32 m2;
  size_t r;

  r = fread(&m1, sizeof(u32), 1, f);
  if (r != 1) {
    printf("fdc: unexpected end of file!\n");
    return -1;
  }

  if (m1 != fdc_magic1) {
    printf("fdc: MAGIC 1 does not match!\n");
    return -1;
  }

  fread(&ss, sizeof(long), 1, f);
  if (r != 1) {
    printf("fdc: unexpected end of file!\n");
    return -1;
  }

  if (ss != sizeof(state)) {
    printf("fdc: STRUCT SIZE does not match!\n");
    return -1;
  }

  fread(&state, sizeof(state), 1, f);
  if (r != 1) {
    printf("fdc: unexpected end of file!\n");
    return -1;
  }

  r = fread(&m2, sizeof(u32), 1, f);
  if (r != 1) {
    printf("fdc: unexpected end of file!\n");
    return -1;
  }

  if (m2 != fdc_magic2) {
    printf("fdc: MAGIC 1 does not match!\n");
    return -1;
  }

  printf("fdc: %ld bytes restored.\n", ss);
  return 0;
}

void CFloppyController::init() {
  if (theAli) {
    bool hasA = (FDISK(0) != NULL);
    bool hasB = (FDISK(1) != NULL);
    theAli->set_floppy_presence(hasA, hasB);
  }
}

void CFloppyController::do_interrupt() {
  // *shrug* I'll figure this out later.
  state.interrupt = true;
  if (theAli)
    theAli->pic_interrupt(0, 6);
}

void CFloppyController::clear_interrupt() {
  state.interrupt = false;
  if (theAli)
    theAli->pic_deassert(0, 6);
}

u8 CFloppyController::get_status() {
  // bit 7 = RQM data register is ready (0: no access is permitted)
  // bit 6 = 1: transfer from controller to system, 0: sys to controller
  // bit 5 = non dma mode
  // bit 4 = diskette controller is busy
  // bit 3-2 reserved
  // bit 1 = drive 1 is busy (seeking)
  // bit 0 = drive 0 is busy (seeking)

  for (int i = 0; i < 2; i++) {
    if (state.drive[i].seeking > 0)
      state.drive[i].seeking--;
    if (state.drive[i].seeking == 0)
      state.status.seeking[i] = false;
    else
      state.status.seeking[i] = true;
  }

  // we mark the controller busy if a disk is seeking or
  // if there is data waiting to be sent by the controller.
  if (state.status.seeking[0] || state.status.seeking[1] ||
      (state.status.dio && state.status.rqm) || (state.cmd_parms_ptr > 0))
    state.status.busy = true;
  else
    state.status.busy = false;

  printf("FDC Status: %s, %s, %s, %s, %s, %s\n",
         state.status.rqm ? "Data Register Ready" : "No Access",
         state.status.dio ? "C->S" : "S->C",
         state.status.nondma ? "No DMA" : "DMA",
         state.status.busy ? "BUSY" : "not busy",
         state.status.seeking[0] ? "Disk 1 Seeking" : "Disk 1 Idle",
         state.status.seeking[1] ? "Disk 0 Seeking" : "Disk 0 Idle");

  u8 data =
      (state.status.rqm ? 0x80 : 0x00) | (state.status.dio ? 0x40 : 0x00) |
      (state.status.nondma ? 0x20 : 0x00) | (state.status.busy ? 0x10 : 0x00) |
      (state.status.seeking[1] ? 0x02 : 0x00) |
      (state.status.seeking[0] ? 0x01 : 0x00);
  return data;
}
