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

#include <dolphin/CARDPriv.h>
#include <dolphin/OSRtcPriv.h>

u8 GameChoice : (OS_BASE_CACHED | 0x000030E3);

static u32 SectorSizeTable[8] = {
    8 * 1024, 16 * 1024, 32 * 1024, 64 * 1024, 128 * 1024, 256 * 1024, 0, 0,
};

static u32 LatencyTable[8] = {
    4, 8, 16, 32, 64, 128, 256, 512,
};

void __CARDMountCallback(s32 chan, s32 result);
static void DoUnmount(s32 chan, s32 result);

static BOOL IsCard(u32 id) {
  u32 size;
  s32 sectorSize;
  if (id & (0xFFFF0000) && (id != 0x80000004 || __CARDVendorID == 0xFFFF)) {
    return FALSE;
  }

  if ((id & 3) != 0) {
    return FALSE;
  }

  size = id & 0xfc;
  switch (size) {
  case 4:
  case 8:
  case 16:
  case 32:
  case 64:
  case 128:
    break;
  default:
    return FALSE;
    break;
  }

  sectorSize = SectorSizeTable[(id & 0x00003800) >> 11];
  if (sectorSize == 0) {
    return FALSE;
  }

  if ((size * 1024 * 1024 / 8) / sectorSize < 8) {
    return FALSE;
  }

  return TRUE;
}

s32 CARDProbeEx(s32 chan, s32* memSize, s32* sectorSize) {
  u32 id;
  CARDControl* card;
  BOOL enabled;
  s32 result;
  int probe;

  if (chan < 0 || 2 <= chan) {
    return CARD_RESULT_FATAL_ERROR;
  }

  if (GameChoice & 0x80) {
    return CARD_RESULT_NOCARD;
  }

  card = &__CARDBlock[chan];
  enabled = OSDisableInterrupts();

  probe = EXIProbeEx(chan);
  if (probe == -1) {
    result = CARD_RESULT_NOCARD;
  } else if (probe == 0) {
    result = CARD_RESULT_BUSY;
  } else if (card->attached) {
    if (card->mountStep < 1) {
      result = CARD_RESULT_BUSY;
    } else {
      if (memSize) {
        *memSize = card->size;
      }
      if (sectorSize) {
        *sectorSize = card->sectorSize;
      }
      result = CARD_RESULT_READY;
    }
  } else if ((EXIGetState(chan) & 8)) {
    result = CARD_RESULT_WRONGDEVICE;
  } else if (!EXIGetID(chan, 0, &id)) {
    result = CARD_RESULT_BUSY;
  } else if (IsCard(id)) {
    if (memSize) {
      *memSize = (s32)(id & 0xfc);
    }
    if (sectorSize) {
      *sectorSize = SectorSizeTable[(id & 0x00003800) >> 11];
    }
    result = CARD_RESULT_READY;
  } else {
    result = CARD_RESULT_WRONGDEVICE;
  }

  OSRestoreInterrupts(enabled);
  return result;
}

static s32 DoMount(s32 chan) {
  CARDControl* card;
  u32 id;
  u8 status;
  s32 result;
  OSSramEx* sram;
  int i;
  u8 checkSum;
  int step;

  card = &__CARDBlock[chan];

  if (card->mountStep == 0) {
    if (EXIGetID(chan, 0, &id) == 0) {
      result = CARD_RESULT_NOCARD;
    } else if (IsCard(id)) {
      result = CARD_RESULT_READY;
    } else {
      result = CARD_RESULT_WRONGDEVICE;
    }
    if (result < 0) {
      goto error;
    }

    card->cid = id;

    card->size = (u16)(id & 0xFC);
    card->sectorSize = SectorSizeTable[(id & 0x00003800) >> 11];
    card->cBlock = (u16)((card->size * 1024 * 1024 / 8) / card->sectorSize);
    card->latency = LatencyTable[(id & 0x00000700) >> 8];

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

    if (!EXIProbe(chan)) {
      result = CARD_RESULT_NOCARD;
      goto error;
    }

    if (!(status & 0x40)) {
      result = __CARDUnlock(chan, card->id);
      if (result < 0) {
        goto error;
      }

      checkSum = 0;
      sram = __OSLockSramEx();
      for (i = 0; i < 12; i++) {
        sram->flashID[chan][i] = card->id[i];
        checkSum += card->id[i];
      }
      sram->flashIDCheckSum[chan] = (u8)~checkSum;
      __OSUnlockSramEx(TRUE);

      return result;
    } else {
      card->mountStep = 1;

      checkSum = 0;
      sram = __OSLockSramEx();
      for (i = 0; i < 12; i++) {
        checkSum += sram->flashID[chan][i];
      }
      __OSUnlockSramEx(FALSE);
      if (sram->flashIDCheckSum[chan] != (u8)~checkSum) {
        result = CARD_RESULT_IOERROR;
        goto error;
      }
    }
  }

  if (card->mountStep == 1) {
    if (card->cid == 0x80000004) {
      u16 vendorID;

      sram = __OSLockSramEx();
      vendorID = *(u16*)sram->flashID[chan];
      __OSUnlockSramEx(FALSE);

      if (__CARDVendorID == 0xffff || vendorID != __CARDVendorID) {
        result = CARD_RESULT_WRONGDEVICE;
        goto error;
      }
    }

    card->mountStep = 2;

    result = __CARDEnableInterrupt(chan, TRUE);
    if (result < 0) {
      goto error;
    }

    EXISetExiCallback(chan, __CARDExiHandler);
    EXIUnlock(chan);
    DCInvalidateRange(card->workArea, CARD_WORKAREA_SIZE);
  }

  step = card->mountStep - 2;
  result = __CARDRead(chan, (u32)card->sectorSize * step, CARD_SYSTEM_BLOCK_SIZE,
                      (u8*)card->workArea + (CARD_SYSTEM_BLOCK_SIZE * step), __CARDMountCallback);
  if (result < 0) {
    __CARDPutControlBlock(card, result);
  }
  return result;

error:
  EXIUnlock(chan);
  DoUnmount(chan, result);
  return result;
}

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

  card = &__CARDBlock[chan];

  switch (result) {
  case CARD_RESULT_READY:
    if (++card->mountStep < CARD_MAX_MOUNT_STEP) {
      result = DoMount(chan);
      if (0 <= result) {
        return;
      }
    } else {
      result = __CARDVerify(card);
    }
    break;
  case CARD_RESULT_UNLOCKED:
    card->unlockCallback = __CARDMountCallback;
    if (!EXILock(chan, 0, __CARDUnlockedHandler)) {
      return;
    }
    card->unlockCallback = 0;

    result = DoMount(chan);
    if (0 <= result) {
      return;
    }
    break;
  case CARD_RESULT_IOERROR:
  case CARD_RESULT_NOCARD:
    DoUnmount(chan, result);
    break;
  }

  callback = card->apiCallback;
  card->apiCallback = 0;
  __CARDPutControlBlock(card, result);
  callback(chan, result);
}

s32 CARDMountAsync(s32 chan, void* workArea, CARDCallback detachCallback,
                   CARDCallback attachCallback) {
  CARDControl* card;
  BOOL enabled;

  if (chan < 0 || 2 <= chan) {
    return CARD_RESULT_FATAL_ERROR;
  }
  if (GameChoice & 0x80) {
    return CARD_RESULT_NOCARD;
  }
  card = &__CARDBlock[chan];

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

  if (!card->attached && (EXIGetState(chan) & 0x08)) {
    OSRestoreInterrupts(enabled);
    return CARD_RESULT_WRONGDEVICE;
  }

  card->result = CARD_RESULT_BUSY;
  card->workArea = workArea;
  card->extCallback = detachCallback;
  card->apiCallback = attachCallback ? attachCallback : __CARDDefaultApiCallback;
  card->exiCallback = 0;

  if (!card->attached && !EXIAttach(chan, __CARDExtHandler)) {
    card->result = CARD_RESULT_NOCARD;
    OSRestoreInterrupts(enabled);
    return CARD_RESULT_NOCARD;
  }

  card->mountStep = 0;
  card->attached = TRUE;
  EXISetExiCallback(chan, 0);
  OSCancelAlarm(&card->alarm);

  card->currentDir = 0;
  card->currentFat = 0;

  OSRestoreInterrupts(enabled);

  card->unlockCallback = __CARDMountCallback;
  if (!EXILock(chan, 0, __CARDUnlockedHandler)) {
    return CARD_RESULT_READY;
  }
  card->unlockCallback = 0;

  return DoMount(chan);
}

static void DoUnmount(s32 chan, s32 result) {
  CARDControl* card;
  BOOL enabled;

  card = &__CARDBlock[chan];
  enabled = OSDisableInterrupts();
  if (card->attached) {
    EXISetExiCallback(chan, 0);
    EXIDetach(chan);
    OSCancelAlarm(&card->alarm);
    card->attached = FALSE;
    card->result = result;
    card->mountStep = 0;
  }
  OSRestoreInterrupts(enabled);
}

s32 CARDUnmount(s32 chan) {
  CARDControl* card;
  s32 result;

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