#include "JSystem/JSystem.h" // IWYU pragma: keep

#include "JSystem/JUtility/JUTGamePad.h"
#include <cmath>
#include "os_report.h"

#if TARGET_PC
#include "dusk/action_bindings.h"
#endif

DUSK_GAME_DATA u32 JUTGamePad::CRumble::sChannelMask[4] = {
    PAD_CHAN0_BIT,
    PAD_CHAN1_BIT,
    PAD_CHAN2_BIT,
    PAD_CHAN3_BIT,
};

static u32 channel_mask[4] = {PAD_CHAN0_BIT, PAD_CHAN1_BIT, PAD_CHAN2_BIT, PAD_CHAN3_BIT};

DUSK_GAME_DATA JSUList<JUTGamePad> JUTGamePad::mPadList(false);

DUSK_GAME_DATA bool JUTGamePad::mListInitialized;

DUSK_GAME_DATA u8 JUTGamePad::mPadAssign[4];

JUTGamePad::JUTGamePad(EPadPort port) : mRumble(this), mLink(this) {
    mPortNum = port;
    if (mPortNum >= 0) {
        mPadAssign[port]++;
    }

    initList();
    mPadList.append(&mLink);
    update();
    mPadRecord = 0;
    mPadReplay = 0;
    clear();
}

JUTGamePad::~JUTGamePad() {
    if (mPortNum >= 0) {
        mPadAssign[mPortNum]--;
        mPortNum = EPortInvalid;
    }

    mPadList.remove(&mLink);
}

void JUTGamePad::initList() {
    if (!mListInitialized) {
        mPadList.initiate();
        mListInitialized = true;
    }
}

DUSK_GAME_DATA u32 JUTGamePad::sSuppressPadReset;

DUSK_GAME_DATA u8 data_8074CFA4_debug;

DUSK_GAME_DATA s32 JUTGamePad::sAnalogMode;

BOOL JUTGamePad::init() {
    PADSetSpec(PAD_SPEC_5);
    setAnalogMode(3);
    return PADInit();
}

void JUTGamePad::clear() {
    mButtonReset.mReset = false;
    field_0xa8 = 1;
#if TARGET_PC
    mResetHoldFrameCount = 0;
#endif
}

DUSK_GAME_DATA PADStatus JUTGamePad::mPadStatus[4];

DUSK_GAME_DATA JUTGamePad::CButton JUTGamePad::mPadButton[4];

DUSK_GAME_DATA JUTGamePad::CStick JUTGamePad::mPadMStick[4];

DUSK_GAME_DATA JUTGamePad::CStick JUTGamePad::mPadSStick[4];

DUSK_GAME_DATA JUTGamePad::EStickMode JUTGamePad::sStickMode = EStickMode1;

DUSK_GAME_DATA int JUTGamePad::sClampMode = EClampStick;

DUSK_GAME_DATA u32 JUTGamePad::sRumbleSupported;

u32 JUTGamePad::read() {
    sRumbleSupported = PADRead(mPadStatus);
#if TARGET_PC
   dusk::updateActionBindings();
#endif

    switch (sClampMode) {
    case EClampStick:
        PADClamp(mPadStatus);
        break;
    case EClampCircle:
        PADClampCircle(mPadStatus);
        break;
    }

    u32 bittest;
    u32 reset_mask = 0;
    for (int i = 0; i < 4; i++) {
        bittest = PAD_CHAN0_BIT >> i;

        if (mPadStatus[i].err == 0) {
            PADStatus* pad_status = &mPadStatus[i];
            u32 stick_status;
            stick_status = mPadMStick[i].update(pad_status->stickX, pad_status->stickY, sStickMode, EMainStick, mPadButton[i].mButton) << 0x18;
            stick_status |= (mPadSStick[i].update(pad_status->substickX, pad_status->substickY, sStickMode, ESubStick, mPadButton[i].mButton) << 0x10);
            
            mPadButton[i].update(pad_status, stick_status);
        } else if (mPadStatus[i].err == -1) {
            mPadMStick[i].update(0, 0, sStickMode, EMainStick, 0);
            mPadSStick[i].update(0, 0, sStickMode, ESubStick, 0);
            mPadButton[i].update(NULL, 0);

            if (!(sSuppressPadReset & bittest)) {
                reset_mask |= bittest;
            }
        } else {
            if (data_8074CFA4_debug) {
                OS_REPORT("game pad read error (%d)\n", mPadStatus[i].err);
            }
            mPadButton[i].mTrigger = 0;
            mPadButton[i].mRelease = 0;
            mPadButton[i].mRepeat = 0;
        }
    }

    for (JSUListIterator<JUTGamePad> pad(mPadList.getFirst()); pad != mPadList.getEnd(); ++pad) {
        if (pad->getPadReplay() != NULL && pad->getPadReplay()->isActive()) {
            PADStatus status;
            pad->getPadReplay()->getStatus(&status);

            u32 stick_status;
            stick_status = pad->mMainStick.update(status.stickX, status.stickY, sStickMode,
                                                 EMainStick, pad->mButton.mButton) << 0x18;
            stick_status |= pad->mSubStick.update(status.substickX, status.substickY, sStickMode,
                                                ESubStick, pad->mButton.mButton) << 0x10;

            pad->mButton.update(&status, stick_status);
        } else {
            if (pad->mPortNum == EPortInvalid) {
                pad->assign();
            }
            pad->update();
        }

        if (pad->getPadRecord() != NULL && pad->getPadRecord()->isActive()) {
            if (pad->mPortNum >= 0) {
                int port = pad->mPortNum;
                if (mPadStatus[port].err == 0) {
                    pad->getPadRecord()->write(&mPadStatus[port]);
                }
            }
        }
    }

    if (reset_mask != 0) {
        PADReset(reset_mask);
    }

    checkResetSwitch();
    return sRumbleSupported;
}

void JUTGamePad::assign() {
    for (int i = 0; i < 4; i++) {
        if (mPadStatus[i].err == 0 && mPadAssign[i] == 0) {
            mPortNum = i;
            mPadAssign[i] = 1;
            mPadButton[i].setRepeat(mButton.mRepeatMask, mButton.mRepeatDelay, mButton.mRepeatRate);
            mRumble.clear(this);
            break;
        }
    }
}

DUSK_GAME_DATA u8 JUTGamePad::CRumble::mStatus[4];

DUSK_GAME_DATA u32 JUTGamePad::CRumble::mEnabled;

DUSK_GAME_DATA callbackFn JUTGamePad::C3ButtonReset::sCallback;

DUSK_GAME_DATA void* JUTGamePad::C3ButtonReset::sCallbackArg;

DUSK_GAME_DATA OSTime JUTGamePad::C3ButtonReset::sThreshold = (OSTime)(OS_TIMER_CLOCK / 60) * 30;

DUSK_GAME_DATA bool JUTGamePad::C3ButtonReset::sResetSwitchPushing;

DUSK_GAME_DATA bool JUTGamePad::C3ButtonReset::sResetOccurred;

DUSK_GAME_DATA s32 JUTGamePad::C3ButtonReset::sResetOccurredPort;

void JUTGamePad::checkResetCallback(OSTime holdTime) {
    if (holdTime >= JUTGamePad::C3ButtonReset::sThreshold) {
        JUTGamePad::C3ButtonReset::sResetOccurred = true;
        JUTGamePad::C3ButtonReset::sResetOccurredPort = mPortNum;

        if (JUTGamePad::C3ButtonReset::sCallback != NULL) {
            JUTGamePad::C3ButtonReset::sCallback(mPortNum, JUTGamePad::C3ButtonReset::sCallbackArg);
        }
    }
}

DUSK_GAME_DATA f32 JUTGamePad::CStick::sPressPoint = 0.5f;

DUSK_GAME_DATA f32 JUTGamePad::CStick::sReleasePoint = 0.25f;

DUSK_GAME_DATA u32 JUTGamePad::C3ButtonReset::sResetPattern = PAD_BUTTON_START | PAD_BUTTON_X | PAD_BUTTON_B;

DUSK_GAME_DATA u32 JUTGamePad::C3ButtonReset::sResetMaskPattern = 0x0000FFFF;

void JUTGamePad::update() {
    if (mPortNum != EPortInvalid) {
        if (mPortNum >= 0 && mPortNum < 4) {
            mButton = mPadButton[mPortNum];
            mMainStick = mPadMStick[mPortNum];
            mSubStick = mPadSStick[mPortNum];
            mErrorStatus = mPadStatus[mPortNum].err;
        }

        if (field_0xa8 == 0 || C3ButtonReset::sResetPattern != (mButton.mButton & C3ButtonReset::sResetMaskPattern)) {
            mButtonReset.mReset = false;
        } else if (!JUTGamePad::C3ButtonReset::sResetOccurred) {
            if (mButtonReset.mReset == true) {
#if TARGET_PC
                checkResetCallback(++mResetHoldFrameCount * (OS_TIMER_CLOCK / 30));
#else
                OSTime hold_time = OSGetTime() - mResetHoldStartTime;
                checkResetCallback(hold_time);
#endif
            } else {
                mButtonReset.mReset = true;
#if TARGET_PC
                mResetHoldFrameCount = 0;
#else
                mResetHoldStartTime = OSGetTime();
#endif
            }
        }

        for (JSUListIterator<JUTGamePadLongPress> pad(JUTGamePadLongPress::sPatternList.getFirst()); pad != JUTGamePadLongPress::sPatternList.getEnd(); ++pad) {
            if (pad->isValid()) {
                if (mPortNum >= 0 && mPortNum < 4) {
                    if ((mButton.mButton & pad->getMaskPattern()) == pad->getPattern()) {
                        if (pad->mLongPressStatus[mPortNum] == true) {
                            OSTime hold_time = OSGetTime() - pad->mStartHoldTime[mPortNum];
                            pad->checkCallback(mPortNum, hold_time);
                        } else {
                            pad->mLongPressStatus[mPortNum] = true;
                            pad->mStartHoldTime[mPortNum] = OSGetTime();
                        }
                    } else {
                        pad->mLongPressStatus[mPortNum] = false;
                    }
                }
            }
        }

        if (mPortNum >= 0 && mPortNum < 4) {
            mRumble.update(mPortNum);
        }
    }
}

DUSK_GAME_DATA JSUList<JUTGamePadLongPress> JUTGamePadLongPress::sPatternList(false);

void JUTGamePad::checkResetSwitch() {
    if (!JUTGamePad::C3ButtonReset::sResetOccurred) {
        int unused;
        if (OSGetResetSwitchState()) {
            C3ButtonReset::sResetSwitchPushing = true;
        } else {
            if (C3ButtonReset::sResetSwitchPushing == true) {
                C3ButtonReset::sResetOccurred = true;
                C3ButtonReset::sResetOccurredPort = EPortInvalid;

                if (C3ButtonReset::sCallback != NULL) {
                    C3ButtonReset::sCallback(EPortInvalid, C3ButtonReset::sCallbackArg);
                }
            }
            C3ButtonReset::sResetSwitchPushing = false;
        }
    }
}

void JUTGamePad::clearForReset() {
    CRumble::setEnabled(0);
    recalibrate(PAD_CHAN3_BIT | PAD_CHAN2_BIT | PAD_CHAN1_BIT | PAD_CHAN0_BIT);
}

void JUTGamePad::CButton::clear() {
    mButton = 0;
    mTrigger = 0;
    mRelease = 0;
    mRepeat = 0;
    mAnalogA = 0;
    mAnalogB = 0;
    mAnalogL = 0;
    mAnalogR = 0;
    mRepeatCount = 0;
    mRepeatStart = 0;
    mRepeatMask = 0;
    mRepeatDelay = 0;
    mRepeatRate = 0;
}

void JUTGamePad::CButton::update(const PADStatus* padStatus, u32 stickStatus) {
    u32 buttons = stickStatus | (padStatus != NULL ? padStatus->button : 0);
    mRepeat = 0;

    if (mRepeatDelay != 0 && mRepeatMask != 0) {
        u32 repeatButton = buttons & mRepeatMask;
        mRepeat = 0;

        if (repeatButton == 0) {
            mRepeatStart = 0;
            mRepeatCount = 0;
        } else if (mRepeatStart == repeatButton) {
            mRepeatCount++;

            if (mRepeatCount == mRepeatDelay ||
                (mRepeatCount > mRepeatDelay && (mRepeatCount - mRepeatDelay) % mRepeatRate == 0))
            {
                mRepeat = repeatButton;
            }
        } else {
            mRepeat = repeatButton & (mRepeatStart ^ 0xFFFFFFFF);
            mRepeatStart = repeatButton;
            mRepeatCount = 0;
        }
    }

    mTrigger = buttons & (buttons ^ mButton);
    mRelease = mButton & (buttons ^ mButton);
    mButton = buttons;
    mRepeat |= (mRepeatMask ^ 0xFFFFFFFF) & mTrigger;

    if (padStatus != NULL) {
        mAnalogA = padStatus->analogA;
        mAnalogB = padStatus->analogB;
        mAnalogL = padStatus->triggerLeft;
        mAnalogR = padStatus->triggerRight;
    } else {
        mAnalogA = 0;
        mAnalogB = 0;
        mAnalogL = 0;
        mAnalogR = 0;
    }

    mAnalogLf = (s32)mAnalogL / 150.0f;
    mAnalogRf = (s32)mAnalogR / 150.0f;
}

void JUTGamePad::CStick::clear() {
    mPosX = 0.0f;
    mPosY = 0.0f;
    mValue = 0.0f;
    mAngle = 0;
}

u32 JUTGamePad::CStick::update(s8 x, s8 y, JUTGamePad::EStickMode mode,
                               JUTGamePad::EWhichStick stick, u32 buttons) {
    s32 clamp;
    switch (getClampMode()) {
    case EClampStick:
        clamp = stick == EMainStick ? 54 : 42;
        break;
    case EClampCircle:
        clamp = stick == EMainStick ? 38 : 29;
        break;
    default:
        clamp = stick == EMainStick ? 69 : 57;
        break;
    }

    mRawX = x;
    mRawY = y;
    mPosX = (f32)x / (f32)clamp;
    mPosY = (f32)y / (f32)clamp;
    mValue = sqrtf((mPosX * mPosX) + (mPosY * mPosY));

    if (mValue > 1.0f) {
        if (mode == EStickMode1) {
            mPosX /= mValue;
            mPosY /= mValue;
        }
        mValue = 1.0f;
    }

    if (mValue > 0.0f) {
        if (mPosY == 0.0f) {
            if (mPosX > 0.0f) {
                mAngle = 0x4000;
            } else {
                mAngle = -0x4000;
            }
        } else {
            mAngle = (0x8000 / 3.1415926f) * atan2f(mPosX, -mPosY);
        }
    }

    return getButton(buttons >> (stick == EMainStick ? 0x18 : 0x10));
}

u32 JUTGamePad::CStick::getButton(u32 buttons) {
    u32 button = buttons & (PAD_BUTTON_UP | PAD_BUTTON_DOWN | PAD_BUTTON_LEFT | PAD_BUTTON_RIGHT);

    if (-sReleasePoint < mPosX && mPosX < sReleasePoint) {
        button &= ~(PAD_BUTTON_LEFT | PAD_BUTTON_RIGHT);
    } else if (mPosX <= -sPressPoint) {
        button &= ~PAD_BUTTON_RIGHT;
        button |= PAD_BUTTON_LEFT;
    } else if (mPosX >= sPressPoint) {
        button &= ~PAD_BUTTON_LEFT;
        button |= PAD_BUTTON_RIGHT;
    }

    if (-sReleasePoint < mPosY && mPosY < sReleasePoint) {
        button &= ~(PAD_BUTTON_UP | PAD_BUTTON_DOWN);
    } else if (mPosY <= -sPressPoint) {
        button &= ~PAD_BUTTON_UP;
        button |= PAD_BUTTON_DOWN;
    } else if (mPosY >= sPressPoint) {
        button &= ~PAD_BUTTON_DOWN;
        button |= PAD_BUTTON_UP;
    }

    return button;
}

void JUTGamePad::CRumble::clear() {
    mFrame = 0;
    mLength = 0;
    mPattern = NULL;
    mFrameCount = 0;
    field_0x10 = 0;
    mEnabled = (PAD_CHAN3_BIT | PAD_CHAN2_BIT | PAD_CHAN1_BIT | PAD_CHAN0_BIT);
}

void JUTGamePad::CRumble::clear(JUTGamePad* pad) {
    if (pad->getPortNum() >= 0 && pad->getPortNum() < 4) {
        mStatus[pad->getPortNum()] = false;
        pad->stopMotorHard();
    }
    clear();
}

void JUTGamePad::CRumble::startMotor(int port) {
    if (isEnabledPort(port)) {
        PADControlMotor(port, PAD_MOTOR_RUMBLE);
        mStatus[port] = true;
    }
}

void JUTGamePad::CRumble::stopMotor(int port, bool hard_stop) {
    u8 command;
    if (isEnabledPort(port)) {
        if (hard_stop) {
            command = PAD_MOTOR_STOP_HARD;
        } else {
            command = PAD_MOTOR_STOP;
        }
        PADControlMotor(port, command);
        mStatus[port] = false;
    }
}

static bool getNumBit(u8* pattern, int index) {
    u8 bit = pattern[index >> 3] & (0x80 >> (index & 7));
    return bit != 0;
}

void JUTGamePad::CRumble::update(s16 port) {
    if (isEnabledPort(port) == false) {
        mFrame = 0;
        mLength = 0;
        mPattern = NULL;
        mFrameCount = 0;
        field_0x10 = NULL;
    }

    if (mLength == 0) {
        return;
    }

    if (mFrame >= mLength) {
        stopMotorHard(port);
        mLength = 0;
    } else if (mFrameCount == 0) {
        if (mStatus[port] == false) {
            startMotor(port);
        }
        return;
    } else {
        bool enabled = getNumBit(mPattern, mFrame % mFrameCount);
        u32 status = mStatus[port] != false;

        if (enabled && !status) {
            startMotor(port);
        } else if (!enabled) {
            bool hard_stop = false;
            if (field_0x10) {
                hard_stop = getNumBit(field_0x10, mFrame % mFrameCount);
            }

            if (status) {
                stopMotor(port, hard_stop);
            } else if (hard_stop) {
                stopMotor(port, true);
            }
        }
    }

    mFrame++;
}

void JUTGamePad::CRumble::triggerPatternedRumble(u32 length) {
    if (mPattern != NULL && mFrameCount != 0) {
        mLength = length;
        mFrame = 0;
    }
}

void JUTGamePad::CRumble::startPatternedRumble(void* data, JUTGamePad::CRumble::ERumble rumble,
                                               u32 length) {
    mFrameCount = ((*(u8*)data) << 8) + *((u8*)data + 1);
    mPattern = (u8*)data + 2;
    
    switch (rumble) {
    case JUTGamePad::CRumble::VAL_0:
        triggerPatternedRumble(mFrameCount);
        break;
    case JUTGamePad::CRumble::VAL_1:
        triggerPatternedRumble(-1);
        break;
    case JUTGamePad::CRumble::VAL_2:
        triggerPatternedRumble(length);
        break;
    }
}

void JUTGamePad::CRumble::stopPatternedRumble(s16 port) {
    JUT_ASSERT(1341, 0 <= port && port < 4);
    mLength = 0;
    stopMotorHard(port);
}

void JUTGamePad::CRumble::stopPatternedRumbleAtThePeriod() {
#if TARGET_PC
    if (mFrameCount == 0) {
        // Does not trap on hardware
        // PowerPC spec says result is "undefined". Let's just write zero.
        mLength = 0;
        return;
    }
#endif
    u32 r31 = mFrame % mFrameCount;
    mLength = (mFrame + mFrameCount - 1) % mFrameCount;
}

JUTGamePad* JUTGamePad::getGamePad(int port) {
    for (JSUListIterator<JUTGamePad> pad(mPadList.getFirst()); pad != mPadList.getEnd(); ++pad) {
        if (port == pad->mPortNum) {
            return pad.getObject();
        }
    }

    return NULL;
}

void JUTGamePad::CRumble::setEnabled(u32 mask) {
    mask = (mask & (PAD_CHAN3_BIT | PAD_CHAN2_BIT | PAD_CHAN1_BIT | PAD_CHAN0_BIT));
    for (int i = 0; i < 4; i++) {
        if ((mEnabled & channel_mask[i]) == 0) {
            if (mStatus[i]) {
                stopMotor(i);
            }

            JUTGamePad* pad = getGamePad(i);
            if (pad != NULL) {
                pad->stopMotorWaveHard();
            }
        }
    }

    mEnabled = mask;
}

void JUTGamePad::CButton::setRepeat(u32 mask, u32 delay, u32 rate) {
    mRepeatStart = 0;
    mRepeatCount = 0;
    mRepeatMask = mask;
    mRepeatDelay = delay;
    mRepeatRate = rate;
}

void JUTGamePad::setButtonRepeat(u32 mask, u32 delay, u32 rate) {
    mButton.setRepeat(mask, delay, rate);
    if (mPortNum >= 0) {
        mPadButton[mPortNum].setRepeat(mask, delay, rate);
    }
}

bool JUTGamePad::recalibrate(u32 mask) {
    for (int i = 0; i < 4; i++) {
        if (sSuppressPadReset & channel_mask[i]) {
            mask &= channel_mask[i] ^ 0xFFFFFFFF;
        }
    }

    BOOL result = PADRecalibrate(mask);
    return result;
}

void JUTGamePadLongPress::checkCallback(int port, u32 hold_time) {
    if (port < 0) {
        return;
    }

    JUT_ASSERT(1673, 0 <= port && port < 4);

    if (hold_time < mThreshold) {
        return;
    }

    field_0x11 = true;
    field_0x48[port] = true;

    if (mCallback != NULL) {
        mCallback(port, this, field_0x50);
    }
}
