#include <dolphin/card.h>
#include <dolphin/dsp.h>
#include <dolphin/dvd.h>
#include <dolphin/os.h>

#include <dolphin/CARDPriv.h>

const char* __CARDVersion =
    "<< Dolphin SDK - CARD\trelease build: Sep  5 2002 05:35:20 (0x2301) >>";

CARDControl __CARDBlock[2];
DVDDiskID __CARDDiskNone;

static u16 __CARDEncode;

s32 __CARDReadStatus(s32 chan, u8* status);
s32 __CARDClearStatus(s32 chan);
void __CARDSetDiskID(const DVDDiskID* id);
static s32 Retry(s32 chan);

BOOL OnReset(BOOL f);
static OSResetFunctionInfo ResetFunctionInfo = {OnReset, 127};

void __CARDDefaultApiCallback(s32 chan, s32 result) {}

void __CARDExtHandler(s32 chan, OSContext* context) {
  CARDControl* card;
  CARDCallback callback;

  card = &__CARDBlock[chan];
  if (card->attached) {
    card->attached = FALSE;
    EXISetExiCallback(chan, 0);
    OSCancelAlarm(&card->alarm);
    callback = card->exiCallback;

    if (callback) {
      card->exiCallback = 0;
      callback(chan, CARD_RESULT_NOCARD);
    }

    if (card->result != CARD_RESULT_BUSY) {
      card->result = CARD_RESULT_NOCARD;
    }

    callback = card->extCallback;
    if (callback && CARD_MAX_MOUNT_STEP <= card->mountStep) {
      card->extCallback = 0;
      callback(chan, CARD_RESULT_NOCARD);
    }
  }
}

void __CARDExiHandler(s32 chan, OSContext* context) {
  CARDControl* card;
  CARDCallback callback;
  u8 status;
  s32 result;

  card = &__CARDBlock[chan];

  OSCancelAlarm(&card->alarm);

  if (!card->attached) {
    return;
  }

  if (!EXILock(chan, 0, 0)) {
    result = CARD_RESULT_FATAL_ERROR;
    goto fatal;
  }

  if ((result = __CARDReadStatus(chan, &status)) < 0 || (result = __CARDClearStatus(chan)) < 0) {
    goto error;
  }

  if ((result = (status & 0x18) ? CARD_RESULT_IOERROR : CARD_RESULT_READY) == CARD_RESULT_IOERROR &&
      --card->retry > 0) {
    result = Retry(chan);
    if (result >= 0) {
      return;
    }
    goto fatal;
  }

error:
  EXIUnlock(chan);

fatal:
  callback = card->exiCallback;
  if (callback) {
    card->exiCallback = 0;
    callback(chan, result);
  }
}

void __CARDTxHandler(s32 chan, OSContext* context) {
  CARDControl* card;
  CARDCallback callback;
  BOOL err;

  card = &__CARDBlock[chan];
  err = !EXIDeselect(chan);
  EXIUnlock(chan);
  callback = card->txCallback;
  if (callback) {
    card->txCallback = 0;
    callback(chan, (!err && EXIProbe(chan)) ? CARD_RESULT_READY : CARD_RESULT_NOCARD);
  }
}

void __CARDUnlockedHandler(s32 chan, OSContext* context) {
  CARDControl* card;
  CARDCallback callback;

  card = &__CARDBlock[chan];
  callback = card->unlockCallback;
  if (callback) {
    card->unlockCallback = 0;
    callback(chan, EXIProbe(chan) ? CARD_RESULT_UNLOCKED : CARD_RESULT_NOCARD);
  }
}

s32 __CARDEnableInterrupt(s32 chan, BOOL enable) {
  BOOL err;
  u32 cmd;

  if (!EXISelect(chan, 0, 4)) {
    return CARD_RESULT_NOCARD;
  }

  cmd = enable ? 0x81010000 : 0x81000000;
  err = FALSE;
  err |= !EXIImm(chan, &cmd, 2, 1, NULL);
  err |= !EXISync(chan);
  err |= !EXIDeselect(chan);
  return err ? CARD_RESULT_NOCARD : CARD_RESULT_READY;
}

s32 __CARDReadStatus(s32 chan, u8* status) {
  BOOL err;
  u32 cmd;

  if (!EXISelect(chan, 0, 4)) {
    return CARD_RESULT_NOCARD;
  }

  cmd = 0x83000000;
  err = FALSE;
  err |= !EXIImm(chan, &cmd, 2, 1, NULL);
  err |= !EXISync(chan);
  err |= !EXIImm(chan, status, 1, 0, NULL);
  err |= !EXISync(chan);
  err |= !EXIDeselect(chan);
  return err ? CARD_RESULT_NOCARD : CARD_RESULT_READY;
}

s32 __CARDClearStatus(s32 chan) {
  BOOL err;
  u32 cmd;

  if (!EXISelect(chan, 0, 4)) {
    return CARD_RESULT_NOCARD;
  }

  cmd = 0x89000000;
  err = FALSE;
  err |= !EXIImm(chan, &cmd, 1, 1, 0);
  err |= !EXISync(chan);
  err |= !EXIDeselect(chan);

  return err ? CARD_RESULT_NOCARD : CARD_RESULT_READY;
}

static void TimeoutHandler(OSAlarm* alarm, OSContext* context) {
  s32 chan;
  CARDControl* card;
  CARDCallback callback;
  for (chan = 0; chan < 2; ++chan) {
    card = &__CARDBlock[chan];
    if (alarm == &card->alarm) {
      break;
    }
  }

  if (!card->attached) {
    return;
  }

  EXISetExiCallback(chan, NULL);
  callback = card->exiCallback;
  if (callback) {
    card->exiCallback = 0;
    callback(chan, CARD_RESULT_IOERROR);
  }
}

static void SetupTimeoutAlarm(CARDControl* card) {
  OSCancelAlarm(&card->alarm);
  switch (card->cmd[0]) {
  case 0xF2:
    OSSetAlarm(&card->alarm, OSMillisecondsToTicks(100), TimeoutHandler);
    break;
  case 0xF3:
    break;
  case 0xF4:
  case 0xF1:
    OSSetAlarm(&card->alarm, OSSecondsToTicks((OSTime)2) * (card->sectorSize / 0x2000),
               TimeoutHandler);
    break;
  }
}

static s32 Retry(s32 chan) {
  CARDControl* card;
  card = &__CARDBlock[chan];

  if (!EXISelect(chan, 0, 4)) {
    EXIUnlock(chan);
    return CARD_RESULT_NOCARD;
  }

  SetupTimeoutAlarm(card);

  if (!EXIImmEx(chan, card->cmd, card->cmdlen, 1)) {
    EXIDeselect(chan);
    EXIUnlock(chan);
    return CARD_RESULT_NOCARD;
  }

  if (card->cmd[0] == 0x52 &&
      !EXIImmEx(chan, (u8*)card->workArea + sizeof(CARDID), card->latency, 1)) {
    EXIDeselect(chan);
    EXIUnlock(chan);
    return CARD_RESULT_NOCARD;
  }

  if (card->mode == 0xffffffff) {
    EXIDeselect(chan);
    EXIUnlock(chan);
    return CARD_RESULT_READY;
  }

  if (!EXIDma(chan, card->buffer, (s32)((card->cmd[0] == 0x52) ? 512 : 128), card->mode,
              __CARDTxHandler)) {
    EXIDeselect(chan);
    EXIUnlock(chan);
    return CARD_RESULT_NOCARD;
  }

  return CARD_RESULT_READY;
}

static void UnlockedCallback(s32 chan, s32 result) {
  CARDCallback callback;
  CARDControl* card;

  card = &__CARDBlock[chan];
  if (result >= 0) {
    card->unlockCallback = UnlockedCallback;
    if (!EXILock(chan, 0, __CARDUnlockedHandler)) {
      result = CARD_RESULT_READY;
    } else {
      card->unlockCallback = 0;
      result = Retry(chan);
    }
  }

  if (result < 0) {
    switch (card->cmd[0]) {
    case 0x52:
      callback = card->txCallback;
      if (callback) {
        card->txCallback = 0;
        callback(chan, result);
      }

      break;
    case 0xF2:
    case 0xF4:
    case 0xF1:
      callback = card->exiCallback;
      if (callback) {
        card->exiCallback = 0;
        callback(chan, result);
      }
      break;
    }
  }
}

static s32 __CARDStart(s32 chan, CARDCallback txCallback, CARDCallback exiCallback) {
  BOOL enabled;
  CARDControl* card;
  s32 result;

  enabled = OSDisableInterrupts();

  card = &__CARDBlock[chan];
  if (!card->attached) {
    result = CARD_RESULT_NOCARD;
  } else {

    if (txCallback) {
      card->txCallback = txCallback;
    }
    if (exiCallback) {
      card->exiCallback = exiCallback;
    }
    card->unlockCallback = UnlockedCallback;
    if (!EXILock(chan, 0, __CARDUnlockedHandler)) {
      result = CARD_RESULT_BUSY;
    } else {
      card->unlockCallback = 0;

      if (!EXISelect(chan, 0, 4)) {
        EXIUnlock(chan);
        result = CARD_RESULT_NOCARD;
      } else {
        SetupTimeoutAlarm(card);
        result = CARD_RESULT_READY;
      }
    }
  }

  OSRestoreInterrupts(enabled);
  return result;
}

#define AD1(x) ((u8)(((x) >> 17) & 0x7f))
#define AD1EX(x) ((u8)(AD1(x) | 0x80));
#define AD2(x) ((u8)(((x) >> 9) & 0xff))
#define AD3(x) ((u8)(((x) >> 7) & 0x03))
#define BA(x) ((u8)((x)&0x7f))

s32 __CARDReadSegment(s32 chan, CARDCallback callback) {
  CARDControl* card;
  s32 result;

  card = &__CARDBlock[chan];
  card->cmd[0] = 0x52;
  card->cmd[1] = AD1(card->addr);
  card->cmd[2] = AD2(card->addr);
  card->cmd[3] = AD3(card->addr);
  card->cmd[4] = BA(card->addr);
  card->cmdlen = 5;
  card->mode = 0;
  card->retry = 0;

  result = __CARDStart(chan, callback, 0);
  if (result == CARD_RESULT_BUSY) {
    result = CARD_RESULT_READY;
  } else if (result >= 0) {
    if (!EXIImmEx(chan, card->cmd, card->cmdlen, 1) ||
        !EXIImmEx(chan, (u8*)card->workArea + sizeof(CARDID), card->latency,
                  1) || // XXX use DMA if possible
        !EXIDma(chan, card->buffer, 512, card->mode, __CARDTxHandler)) {
      card->txCallback = 0;
      EXIDeselect(chan);
      EXIUnlock(chan);
      result = CARD_RESULT_NOCARD;
    } else {
      result = CARD_RESULT_READY;
    }
  }
  return result;
}

s32 __CARDWritePage(s32 chan, CARDCallback callback) {
  CARDControl* card;
  s32 result;

  card = &__CARDBlock[chan];
  card->cmd[0] = 0xF2;
  card->cmd[1] = AD1(card->addr);
  card->cmd[2] = AD2(card->addr);
  card->cmd[3] = AD3(card->addr);
  card->cmd[4] = BA(card->addr);
  card->cmdlen = 5;
  card->mode = 1;
  card->retry = 3;

  result = __CARDStart(chan, 0, callback);
  if (result == CARD_RESULT_BUSY) {
    result = CARD_RESULT_READY;
  } else if (result >= 0) {
    if (!EXIImmEx(chan, card->cmd, card->cmdlen, 1) ||
        !EXIDma(chan, card->buffer, 128, card->mode, __CARDTxHandler)) {
      card->exiCallback = 0;
      EXIDeselect(chan);
      EXIUnlock(chan);
      result = CARD_RESULT_NOCARD;
    } else {
      result = CARD_RESULT_READY;
    }
  }
  return result;
}

s32 __CARDEraseSector(s32 chan, u32 addr, CARDCallback callback) {
  CARDControl* card;
  s32 result;

  card = &__CARDBlock[chan];
  card->cmd[0] = 0xF1;
  card->cmd[1] = AD1(addr);
  card->cmd[2] = AD2(addr);
  card->cmdlen = 3;
  card->mode = -1;
  card->retry = 3;

  result = __CARDStart(chan, 0, callback);

  if (result == CARD_RESULT_BUSY) {
    result = CARD_RESULT_READY;
  } else if (result >= 0) {
    if (!EXIImmEx(chan, card->cmd, card->cmdlen, 1)) {
      card->exiCallback = NULL;
      result = CARD_RESULT_NOCARD;
    } else {
      result = CARD_RESULT_READY;
    }

    EXIDeselect(chan);
    EXIUnlock(chan);
  }
  return result;
}

void CARDInit(void) {
  int chan;

  if (__CARDBlock[0].diskID && __CARDBlock[1].diskID) {
    return;
  }

  __CARDEncode = OSGetFontEncode();

  OSRegisterVersion(__CARDVersion);

  DSPInit();
  OSInitAlarm();

  for (chan = 0; chan < 2; ++chan) {
    CARDControl* card = &__CARDBlock[chan];

    card->result = CARD_RESULT_NOCARD;
    OSInitThreadQueue(&card->threadQueue);
    OSCreateAlarm(&card->alarm);
  }
  __CARDSetDiskID((DVDDiskID*)OSPhysicalToCached(0x0));

  OSRegisterResetFunction(&ResetFunctionInfo);
}

u16 __CARDGetFontEncode() { return __CARDEncode; }

void __CARDSetDiskID(const DVDDiskID* id) {
  __CARDBlock[0].diskID = id ? id : &__CARDDiskNone;
  __CARDBlock[1].diskID = id ? id : &__CARDDiskNone;
}

s32 __CARDGetControlBlock(s32 chan, CARDControl** pcard) {
  BOOL enabled;
  s32 result;
  CARDControl* card;

  card = &__CARDBlock[chan];
  if (chan < 0 || chan >= 2 || card->diskID == NULL) {
    return CARD_RESULT_FATAL_ERROR;
  }

  enabled = OSDisableInterrupts();
  if (!card->attached) {
    result = CARD_RESULT_NOCARD;
  } else if (card->result == CARD_RESULT_BUSY) {
    result = CARD_RESULT_BUSY;
  } else {
    card->result = CARD_RESULT_BUSY;
    result = CARD_RESULT_READY;
    card->apiCallback = 0;
    *pcard = card;
  }
  OSRestoreInterrupts(enabled);
  return result;
}

s32 __CARDPutControlBlock(CARDControl* card, s32 result) {
  BOOL enabled;

  enabled = OSDisableInterrupts();
  if (card->attached) {
    card->result = result;
  } else if (card->result == CARD_RESULT_BUSY) {
    card->result = result;
  }
  OSRestoreInterrupts(enabled);
  return result;
}

s32 CARDGetResultCode(s32 chan) {
  CARDControl* card;
  if (chan < 0 || chan >= 2) {
    return CARD_RESULT_FATAL_ERROR;
  }
  card = &__CARDBlock[chan];
  return card->result;
}

s32 CARDFreeBlocks(s32 chan, s32* byteNotUsed, s32* filesNotUsed) {
  CARDControl* card;
  s32 result;
  u16* fat;
  CARDDir* dir;
  CARDDir* ent;
  u16 fileNo;

  result = __CARDGetControlBlock(chan, &card);
  if (result < 0) {
    return result;
  }

  fat = __CARDGetFatBlock(card);
  dir = __CARDGetDirBlock(card);
  if (fat == 0 || dir == 0) {
    return __CARDPutControlBlock(card, CARD_RESULT_BROKEN);
  }

  if (byteNotUsed) {
    *byteNotUsed = (s32)(card->sectorSize * fat[CARD_FAT_FREEBLOCKS]);
  }

  if (filesNotUsed) {
    *filesNotUsed = 0;
    for (fileNo = 0; fileNo < CARD_MAX_FILE; fileNo++) {
      ent = &dir[fileNo];
      if (ent->fileName[0] == 0xff) {
        ++*filesNotUsed;
      }
    }
  }

  return __CARDPutControlBlock(card, CARD_RESULT_READY);
}

static BOOL OnReset(BOOL f) {
  if (!f) {
    if (CARDUnmount(0) == CARD_RESULT_BUSY || CARDUnmount(1) == CARD_RESULT_BUSY) {
      return FALSE;
    }
  }

  return TRUE;
}
