#include "dolphin/os.h"
#include "dolphin/hw_regs.h"

#pragma scheduling off

static const char* __EXIVersion =
    "<< Dolphin SDK - EXI\trelease build: Sep  5 2002 05:33:04 (0x2301) >>";

#define MAX_DEV 3
#define MAX_CHAN 3

#define REG_MAX 5
#define REG(chan, idx) (__EXIRegs[((chan)*REG_MAX) + (idx)])

#define STATE_IDLE 0x00
#define STATE_DMA 0x01
#define STATE_IMM 0x02
#define STATE_BUSY (STATE_DMA | STATE_IMM)
#define STATE_SELECTED 0x04
#define STATE_ATTACHED 0x08
#define STATE_LOCKED 0x10

#define EXI_0CR(tstart, dma, rw, tlen)                                                             \
  ((((u32)(tstart)) << 0) | (((u32)(dma)) << 1) | (((u32)(rw)) << 2) | (((u32)(tlen)) << 4))

#define CPR_CS(x) ((1u << (x)) << 7)
#define CPR_CLK(x) ((x) << 4)

typedef struct EXIControl {
  EXICallback exiCallback;
  EXICallback tcCallback;
  EXICallback extCallback;
  vu32 state;
  int immLen;
  u8* immBuf;
  u32 dev;
  u32 id;
  s32 idTime;
  int items;
  struct {
    u32 dev;
    EXICallback callback;
  } queue[MAX_DEV];
} EXIControl;

static EXIControl Ecb[MAX_CHAN];

s32 __EXIProbeStartTime[2] : (OS_BASE_CACHED | 0x30C0);

static void SetExiInterruptMask(s32 chan, EXIControl* exi) {
  EXIControl* exi2;

  exi2 = &Ecb[2];
  switch (chan) {
  case 0:
    if ((exi->exiCallback == 0 && exi2->exiCallback == 0) || (exi->state & STATE_LOCKED)) {
      __OSMaskInterrupts(OS_INTERRUPTMASK_EXI_0_EXI | OS_INTERRUPTMASK_EXI_2_EXI);
    } else {
      __OSUnmaskInterrupts(OS_INTERRUPTMASK_EXI_0_EXI | OS_INTERRUPTMASK_EXI_2_EXI);
    }
    break;
  case 1:
    if (exi->exiCallback == 0 || (exi->state & STATE_LOCKED)) {
      __OSMaskInterrupts(OS_INTERRUPTMASK_EXI_1_EXI);
    } else {
      __OSUnmaskInterrupts(OS_INTERRUPTMASK_EXI_1_EXI);
    }
    break;
  case 2:
    if (__OSGetInterruptHandler(__OS_INTERRUPT_PI_DEBUG) == 0 || (exi->state & STATE_LOCKED)) {
      __OSMaskInterrupts(OS_INTERRUPTMASK_PI_DEBUG);
    } else {
      __OSUnmaskInterrupts(OS_INTERRUPTMASK_PI_DEBUG);
    }
    break;
  }
}

static void CompleteTransfer(s32 chan) {
  EXIControl* exi = &Ecb[chan];
  u8* buf;
  u32 data;
  int i;
  int len;

  if (exi->state & STATE_BUSY) {
    if ((exi->state & STATE_IMM) && (len = exi->immLen)) {
      buf = exi->immBuf;
      data = REG(chan, 4);
      for (i = 0; i < len; i++) {
        *buf++ = (u8)((data >> ((3 - i) * 8)) & 0xff);
      }
    }
    exi->state &= ~STATE_BUSY;
  }
}

BOOL EXIImm(s32 chan, void* buf, s32 len, u32 type, EXICallback callback) {
  EXIControl* exi = &Ecb[chan];
  BOOL enabled;

  enabled = OSDisableInterrupts();
  if ((exi->state & STATE_BUSY) || !(exi->state & STATE_SELECTED)) {
    OSRestoreInterrupts(enabled);
    return FALSE;
  }

  exi->tcCallback = callback;
  if (exi->tcCallback) {
    EXIClearInterrupts(chan, FALSE, TRUE, FALSE);
    __OSUnmaskInterrupts(OS_INTERRUPTMASK_EXI_0_TC >> (3 * chan));
  }

  exi->state |= STATE_IMM;

  if (type != EXI_READ) {
    u32 data;
    int i;

    data = 0;
    for (i = 0; i < len; i++) {
      data |= ((u8*)buf)[i] << ((3 - i) * 8);
    }
    REG(chan, 4) = data;
  }

  exi->immBuf = buf;
  exi->immLen = (type != EXI_WRITE) ? len : 0;

  REG(chan, 3) = EXI_0CR(1, 0, type, len - 1);

  OSRestoreInterrupts(enabled);

  return TRUE;
}

BOOL EXIImmEx(s32 chan, void* buf, s32 len, u32 mode) {
  s32 xLen;

  while (len) {
    xLen = (len < 4) ? len : 4;
    if (!EXIImm(chan, buf, xLen, mode, NULL)) {
      return FALSE;
    }

    if (!EXISync(chan)) {
      return FALSE;
    }

    (u8*)buf += xLen;
    len -= xLen;
  }
  return TRUE;
}

BOOL EXIDma(s32 chan, void* buf, s32 len, u32 type, EXICallback callback) {
  EXIControl* exi = &Ecb[chan];
  BOOL enabled;

  enabled = OSDisableInterrupts();
  if ((exi->state & STATE_BUSY) || !(exi->state & STATE_SELECTED)) {
    OSRestoreInterrupts(enabled);
    return FALSE;
  }

  exi->tcCallback = callback;
  if (exi->tcCallback) {
    EXIClearInterrupts(chan, FALSE, TRUE, FALSE);
    __OSUnmaskInterrupts(OS_INTERRUPTMASK_EXI_0_TC >> (3 * chan));
  }

  exi->state |= STATE_DMA;

  REG(chan, 1) = (u32)buf & 0x3ffffe0;
  REG(chan, 2) = (u32)len;
  REG(chan, 3) = EXI_0CR(1, 1, type, 0);

  OSRestoreInterrupts(enabled);

  return TRUE;
}

extern u32 __OSGetDIConfig(void);

vu16 __OSDeviceCode : (OS_BASE_CACHED | 0x30E6);

BOOL EXISync(s32 chan) {
  EXIControl* exi = &Ecb[chan];
  BOOL rc = FALSE;
  BOOL enabled;

  while (exi->state & STATE_SELECTED) {
    if (((REG(chan, 3) & 1) >> 0) == 0) {
      enabled = OSDisableInterrupts();
      if (exi->state & STATE_SELECTED) {
        CompleteTransfer(chan);
        if (__OSGetDIConfig() != 0xff || exi->immLen != 4 ||
            (REG(chan, 0) & 0x00000070) != (EXI_FREQ_1M << 4) ||
            (REG(chan, 4) != EXI_USB_ADAPTER && REG(chan, 4) != EXI_IS_VIEWER &&
             REG(chan, 4) != 0x04220001) ||
            __OSDeviceCode == 0x8200) {
          rc = TRUE;
        }
      }
      OSRestoreInterrupts(enabled);
      break;
    }
  }
  return rc;
}

u32 EXIClearInterrupts(s32 chan, BOOL exi, BOOL tc, BOOL ext) {
  u32 cpr;
  u32 prev;

  prev = cpr = REG(chan, 0);
  cpr &= 0x7f5;
  if (exi)
    cpr |= 2;
  if (tc)
    cpr |= 8;
  if (ext)
    cpr |= 0x800;
  REG(chan, 0) = cpr;
  return prev;
}

EXICallback EXISetExiCallback(s32 chan, EXICallback exiCallback) {
  EXIControl* exi = &Ecb[chan];
  EXICallback prev;
  BOOL enabled;

  enabled = OSDisableInterrupts();
  prev = exi->exiCallback;
  exi->exiCallback = exiCallback;

  if (chan != 2) {
    SetExiInterruptMask(chan, exi);
  } else {
    SetExiInterruptMask(0, &Ecb[0]);
  }

  OSRestoreInterrupts(enabled);
  return prev;
}

void EXIProbeReset(void) {
  __EXIProbeStartTime[0] = __EXIProbeStartTime[1] = 0;
  Ecb[0].idTime = Ecb[1].idTime = 0;
  __EXIProbe(0);
  __EXIProbe(1);
}

static BOOL __EXIProbe(s32 chan) {
  EXIControl* exi = &Ecb[chan];
  BOOL enabled;
  BOOL rc;
  u32 cpr;
  s32 t;

  if (chan == 2) {
    return TRUE;
  }

  rc = TRUE;
  enabled = OSDisableInterrupts();
  cpr = REG(chan, 0);
  if (!(exi->state & EXI_STATE_ATTACHED)) {
    if (cpr & 0x00000800) {
      EXIClearInterrupts(chan, FALSE, FALSE, TRUE);
      __EXIProbeStartTime[chan] = exi->idTime = 0;
    }

    if (cpr & 0x00001000) {
      t = (s32)(OSTicksToMilliseconds(OSGetTime()) / 100) + 1;
      if (__EXIProbeStartTime[chan] == 0) {
        __EXIProbeStartTime[chan] = t;
      }
      if (t - __EXIProbeStartTime[chan] < 300 / 100) {
        rc = FALSE;
      }
    } else {
      __EXIProbeStartTime[chan] = exi->idTime = 0;
      rc = FALSE;
    }
  } else if (!(cpr & 0x00001000) || (cpr & 0x00000800)) {
    __EXIProbeStartTime[chan] = exi->idTime = 0;
    rc = FALSE;
  }
  OSRestoreInterrupts(enabled);

  return rc;
}

BOOL EXIProbe(s32 chan) {
  EXIControl* exi = &Ecb[chan];
  BOOL rc;
  u32 id;

  rc = __EXIProbe(chan);
  if (rc && exi->idTime == 0) {
    rc = EXIGetID(chan, 0, &id) ? TRUE : FALSE;
  }
  return rc;
}

s32 EXIProbeEx(s32 chan) {
  if (EXIProbe(chan)) {
    return 1;
  } else if (__EXIProbeStartTime[chan] != 0) {
    return 0;
  } else {
    return -1;
  }
}

static BOOL __EXIAttach(s32 chan, EXICallback extCallback) {
  EXIControl* exi = &Ecb[chan];
  BOOL enabled;

  enabled = OSDisableInterrupts();
  if ((exi->state & EXI_STATE_ATTACHED) || __EXIProbe(chan) == FALSE) {
    OSRestoreInterrupts(enabled);
    return FALSE;
  }

  EXIClearInterrupts(chan, TRUE, FALSE, FALSE);

  exi->extCallback = extCallback;
  __OSUnmaskInterrupts(OS_INTERRUPTMASK_EXI_0_EXT >> (3 * chan));
  exi->state |= STATE_ATTACHED;
  OSRestoreInterrupts(enabled);

  return TRUE;
}

BOOL EXIAttach(s32 chan, EXICallback extCallback) {
  EXIControl* exi = &Ecb[chan];
  BOOL enabled;
  BOOL rc;

  EXIProbe(chan);

  enabled = OSDisableInterrupts();
  if (exi->idTime == 0) {
    OSRestoreInterrupts(enabled);
    return FALSE;
  }
  rc = __EXIAttach(chan, extCallback);
  OSRestoreInterrupts(enabled);
  return rc;
}

BOOL EXIDetach(s32 chan) {
  EXIControl* exi = &Ecb[chan];
  BOOL enabled;

  enabled = OSDisableInterrupts();
  if (!(exi->state & STATE_ATTACHED)) {
    OSRestoreInterrupts(enabled);
    return TRUE;
  }
  if ((exi->state & STATE_LOCKED) && exi->dev == 0) {
    OSRestoreInterrupts(enabled);
    return FALSE;
  }

  exi->state &= ~STATE_ATTACHED;
  __OSMaskInterrupts((OS_INTERRUPTMASK_EXI_0_EXT | OS_INTERRUPTMASK_EXI_0_EXI) >> (3 * chan));
  OSRestoreInterrupts(enabled);
  return TRUE;
}

BOOL EXISelect(s32 chan, u32 dev, u32 freq) {
  EXIControl* exi = &Ecb[chan];
  u32 cpr;
  BOOL enabled;

  enabled = OSDisableInterrupts();
  if ((exi->state & STATE_SELECTED) ||
      chan != 2 && (dev == 0 && !(exi->state & STATE_ATTACHED) && !__EXIProbe(chan) ||
                    !(exi->state & STATE_LOCKED) || (exi->dev != dev))) {
    OSRestoreInterrupts(enabled);
    return FALSE;
  }

  exi->state |= STATE_SELECTED;
  cpr = REG(chan, 0);
  cpr &= 0x405;
  cpr |= CPR_CS(dev) | CPR_CLK(freq);
  REG(chan, 0) = cpr;

  if (exi->state & STATE_ATTACHED) {
    switch (chan) {
    case 0:
      __OSMaskInterrupts(OS_INTERRUPTMASK_EXI_0_EXT);
      break;
    case 1:
      __OSMaskInterrupts(OS_INTERRUPTMASK_EXI_1_EXT);
      break;
    }
  }

  OSRestoreInterrupts(enabled);
  return TRUE;
}

BOOL EXIDeselect(s32 chan) {
  EXIControl* exi = &Ecb[chan];
  u32 cpr;
  BOOL enabled;

  enabled = OSDisableInterrupts();
  if (!(exi->state & STATE_SELECTED)) {
    OSRestoreInterrupts(enabled);
    return FALSE;
  }
  exi->state &= ~STATE_SELECTED;
  cpr = REG(chan, 0);
  REG(chan, 0) = cpr & 0x405;

  if (exi->state & STATE_ATTACHED) {
    switch (chan) {
    case 0:
      __OSUnmaskInterrupts(OS_INTERRUPTMASK_EXI_0_EXT);
      break;
    case 1:
      __OSUnmaskInterrupts(OS_INTERRUPTMASK_EXI_1_EXT);
      break;
    }
  }

  OSRestoreInterrupts(enabled);

  if (chan != 2 && (cpr & CPR_CS(0))) {
    return __EXIProbe(chan) ? TRUE : FALSE;
  }

  return TRUE;
}

static void EXIIntrruptHandler(__OSInterrupt interrupt, OSContext* context) {
  s32 chan;
  EXIControl* exi;
  EXICallback callback;

  chan = (interrupt - __OS_INTERRUPT_EXI_0_EXI) / 3;
  exi = &Ecb[chan];
  EXIClearInterrupts(chan, TRUE, FALSE, FALSE);
  callback = exi->exiCallback;
  if (callback) {
    OSContext exceptionContext;

    OSClearContext(&exceptionContext);
    OSSetCurrentContext(&exceptionContext);

    callback(chan, context);

    OSClearContext(&exceptionContext);
    OSSetCurrentContext(context);
  }
}

static void TCIntrruptHandler(__OSInterrupt interrupt, OSContext* context) {
  OSContext exceptionContext;
  s32 chan;
  EXIControl* exi;
  EXICallback callback;

  chan = (interrupt - __OS_INTERRUPT_EXI_0_TC) / 3;
  exi = &Ecb[chan];
  __OSMaskInterrupts(OS_INTERRUPTMASK(interrupt));
  EXIClearInterrupts(chan, FALSE, TRUE, FALSE);
  callback = exi->tcCallback;
  if (callback) {
    exi->tcCallback = 0;
    CompleteTransfer(chan);

    OSClearContext(&exceptionContext);
    OSSetCurrentContext(&exceptionContext);

    callback(chan, context);

    OSClearContext(&exceptionContext);
    OSSetCurrentContext(context);
  }
}

static void EXTIntrruptHandler(__OSInterrupt interrupt, OSContext* context) {
  s32 chan;
  EXIControl* exi;
  EXICallback callback;

  chan = (interrupt - __OS_INTERRUPT_EXI_0_EXT) / 3;
  __OSMaskInterrupts((OS_INTERRUPTMASK_EXI_0_EXT | OS_INTERRUPTMASK_EXI_0_EXI) >> (3 * chan));
  exi = &Ecb[chan];
  callback = exi->extCallback;
  exi->state &= ~STATE_ATTACHED;
  if (callback) {
    OSContext exceptionContext;

    OSClearContext(&exceptionContext);
    OSSetCurrentContext(&exceptionContext);

    exi->extCallback = 0;
    callback(chan, context);

    OSClearContext(&exceptionContext);
    OSSetCurrentContext(context);
  }
}

void EXIInit(void) {
  OSRegisterVersion(__EXIVersion);

  __OSMaskInterrupts(OS_INTERRUPTMASK_EXI_0_EXI | OS_INTERRUPTMASK_EXI_0_TC |
                     OS_INTERRUPTMASK_EXI_0_EXT | OS_INTERRUPTMASK_EXI_1_EXI |
                     OS_INTERRUPTMASK_EXI_1_TC | OS_INTERRUPTMASK_EXI_1_EXT |
                     OS_INTERRUPTMASK_EXI_2_EXI | OS_INTERRUPTMASK_EXI_2_TC);

  REG(0, 0) = 0;
  REG(1, 0) = 0;
  REG(2, 0) = 0;

  REG(0, 0) = 0x00002000;

  __OSSetInterruptHandler(__OS_INTERRUPT_EXI_0_EXI, EXIIntrruptHandler);
  __OSSetInterruptHandler(__OS_INTERRUPT_EXI_0_TC, TCIntrruptHandler);
  __OSSetInterruptHandler(__OS_INTERRUPT_EXI_0_EXT, EXTIntrruptHandler);
  __OSSetInterruptHandler(__OS_INTERRUPT_EXI_1_EXI, EXIIntrruptHandler);
  __OSSetInterruptHandler(__OS_INTERRUPT_EXI_1_TC, TCIntrruptHandler);
  __OSSetInterruptHandler(__OS_INTERRUPT_EXI_1_EXT, EXTIntrruptHandler);
  __OSSetInterruptHandler(__OS_INTERRUPT_EXI_2_EXI, EXIIntrruptHandler);
  __OSSetInterruptHandler(__OS_INTERRUPT_EXI_2_TC, TCIntrruptHandler);

  if ((OSGetConsoleType() & 0x10000000) != 0) {
    __EXIProbeStartTime[0] = __EXIProbeStartTime[1] = 0;
    Ecb[0].idTime = Ecb[1].idTime = 0;
    __EXIProbe(0);
    __EXIProbe(1);
  }
}

BOOL EXILock(s32 chan, u32 dev, EXICallback unlockedCallback) {
  EXIControl* exi = &Ecb[chan];
  BOOL enabled;
  int i;

  enabled = OSDisableInterrupts();
  if (exi->state & STATE_LOCKED) {
    if (unlockedCallback) {
      for (i = 0; i < exi->items; i++) {
        if (exi->queue[i].dev == dev) {
          OSRestoreInterrupts(enabled);
          return FALSE;
        }
      }
      exi->queue[exi->items].callback = unlockedCallback;
      exi->queue[exi->items].dev = dev;
      exi->items++;
    }
    OSRestoreInterrupts(enabled);
    return FALSE;
  }

  exi->state |= STATE_LOCKED;
  exi->dev = dev;
  SetExiInterruptMask(chan, exi);

  OSRestoreInterrupts(enabled);
  return TRUE;
}

BOOL EXIUnlock(s32 chan) {
  EXIControl* exi = &Ecb[chan];
  BOOL enabled;
  EXICallback unlockedCallback;

  enabled = OSDisableInterrupts();
  if (!(exi->state & STATE_LOCKED)) {
    OSRestoreInterrupts(enabled);
    return FALSE;
  }
  exi->state &= ~STATE_LOCKED;
  SetExiInterruptMask(chan, exi);

  if (0 < exi->items) {
    unlockedCallback = exi->queue[0].callback;
    if (0 < --exi->items) {
      memmove(&exi->queue[0], &exi->queue[1], sizeof(exi->queue[0]) * exi->items);
    }
    unlockedCallback(chan, 0);
  }

  OSRestoreInterrupts(enabled);
  return TRUE;
}

u32 EXIGetState(s32 chan) {
  EXIControl* exi = &Ecb[chan];

  return (u32)exi->state;
}

static void UnlockedHandler(s32 chan, OSContext* context) {
  u32 id;

  EXIGetID(chan, 0, &id);
}

s32 EXIGetID(s32 chan, u32 dev, u32* id) {
  EXIControl* exi = &Ecb[chan];
  BOOL err;
  u32 cmd;
  s32 startTime;
  BOOL enabled;

  if (chan < 2 && dev == 0) {
    if (!__EXIProbe(chan)) {
      return 0;
    }

    if (exi->idTime == __EXIProbeStartTime[chan]) {
      *id = exi->id;
      return exi->idTime;
    }

    if (!__EXIAttach(chan, NULL)) {
      return 0;
    }

    startTime = __EXIProbeStartTime[chan];
  }

  err = !EXILock(chan, dev, (chan < 2 && dev == 0) ? UnlockedHandler : NULL);
  if (!err) {
    err = !EXISelect(chan, dev, EXI_FREQ_1M);
    if (!err) {
      cmd = 0;
      err |= !EXIImm(chan, &cmd, 2, EXI_WRITE, NULL);
      err |= !EXISync(chan);
      err |= !EXIImm(chan, id, 4, EXI_READ, NULL);
      err |= !EXISync(chan);
      err |= !EXIDeselect(chan);
    }
    EXIUnlock(chan);
  }

  if (chan < 2 && dev == 0) {
    EXIDetach(chan);
    enabled = OSDisableInterrupts();
    err |= (startTime != __EXIProbeStartTime[chan]);
    if (!err) {
      exi->id = *id;
      exi->idTime = startTime;
    }
    OSRestoreInterrupts(enabled);

    return err ? 0 : exi->idTime;
  }

  return err ? 0 : !0;
}

char* EXIGetTypeString(u32 type) {
  switch (type) {
  case EXI_MEMORY_CARD_59:
    return "Memory Card 59";
  case EXI_MEMORY_CARD_123:
    return "Memory Card 123";
  case EXI_MEMORY_CARD_251:
    return "Memory Card 251";
  case EXI_MEMORY_CARD_507:
    return "Memory Card 507";
  case EXI_USB_ADAPTER:
    return "USB Adapter";
  case 0x80000000 | EXI_MEMORY_CARD_59:
  case 0x80000000 | EXI_MEMORY_CARD_123:
  case 0x80000000 | EXI_MEMORY_CARD_251:
  case 0x80000000 | EXI_MEMORY_CARD_507:
    return "Net Card";
  case EXI_ETHER_VIEWER:
    return "Artist Ether";
  case EXI_STREAM_HANGER:
    return "Stream Hanger";
  case EXI_IS_VIEWER:
    return "IS Viewer";
  }
}

#pragma scheduling reset
