#include "hid.h"
#include "i2c.h"
#include "timer.h"
#include "colors.h"
#include "screenshot.h" // for screenshots

#include "arm.h"
#include "fixp.h"
#include "shmem.h"

#define HID_TOUCH_MAXPOINT  (0x1000)
#define HID_TOUCH_MIDPOINT  (HID_TOUCH_MAXPOINT / 2)


static void SetNotificationLED(u32 period_ms, u32 rgb565_color)
{
    u32 rgb888_color =
        ((rgb565_color >> 11) << (16+3) |
        (rgb565_color >> 5) << (8+2) |
        (rgb565_color << 3));
    u32 args[] = {period_ms, rgb888_color};
    PXI_DoCMD(PXI_NOTIFY_LED, args, 2);
}

// there's some weird thing going on when reading this
// with an LDRD instruction so for now they'll be two
// separate things - hopefully LTO won't get in the way
u32 HID_ReadState(void)
{
    return ARM_GetSHMEM()->hidState.keys;
}

u32 HID_ReadRawTouchState(void)
{
    return ARM_GetSHMEM()->hidState.touch;
}

// ts_mult indicates a scalar for each axis
// if |ts_mult| > 1 => point must be "scaled out"
// if |ts_mult| < 1 => point must be "scaled in"
// if ts_mult < 0 => axis is inverted
static fixp_t ts_mult[2];

// ts_org indicates the coordinate system origin
static int ts_org[2];

bool HID_TouchCalibratedTransform(u32 ts, u16 *x, u16 *y)
{
    int xc, yc;
    int tx, ty;

    if (ts & BIT(31))
        return false;

    tx = HID_RAW_TX(ts) - HID_TOUCH_MIDPOINT;
    ty = HID_RAW_TY(ts) - HID_TOUCH_MIDPOINT;

    xc = FIXP_TO_INT(fixp_round(tx * ts_mult[0])) + ts_org[0];
    yc = FIXP_TO_INT(fixp_round(ty * ts_mult[1])) + ts_org[1];

    *x = clamp(xc, 0, (ts_org[0] * 2) - 1);
    *y = clamp(yc, 0, (ts_org[1] * 2) - 1);
    return true;
}

bool HID_ReadTouchState(u16 *x, u16 *y)
{
    return HID_TouchCalibratedTransform(HID_ReadRawTouchState(), x, y);
}

bool HID_SetCalibrationData(const HID_CalibrationData *calibs, u32 point_cnt, u32 screen_w, u32 screen_h)
{
    u32 mid_x, mid_y;
    fixp_t avg_x, avg_y;

    if (!screen_w || !screen_h || point_cnt <= 0 || point_cnt > 7)
        return false;

    mid_x = screen_w / 2;
    mid_y = screen_h / 2;

    avg_x = 0;
    avg_y = 0;
    for (u32 i = 0; i < point_cnt; i++) {
        const HID_CalibrationData *data = &calibs[i];
        fixp_t screen_x, screen_y, touch_x, touch_y;

        // translate the [0, screen_w] x [0, screen_h] system
        // to [-screen_w/2, screen_w/2] x [-screen_h/2, screen_h/2]
        screen_x = INT_TO_FIXP(data->screen_x - mid_x);
        screen_y = INT_TO_FIXP(data->screen_y - mid_y);

        // same thing for raw touchscreen data
        touch_x = INT_TO_FIXP(HID_RAW_TX(data->ts_raw) - HID_TOUCH_MIDPOINT);
        touch_y = INT_TO_FIXP(HID_RAW_TY(data->ts_raw) - HID_TOUCH_MIDPOINT);

        // if the data retrieved came right in the middle, it's invalid
        if (!screen_x || !screen_y || !touch_x || !touch_y)
            return false;

        // prevent integer overflows by dividing in this step
        avg_x += fixp_quotient(screen_x, touch_x * point_cnt);
        avg_y += fixp_quotient(screen_y, touch_y * point_cnt);
    }

    // set state variables
    ts_mult[0] = avg_x;
    ts_mult[1] = avg_y;
    ts_org[0] = mid_x;
    ts_org[1] = mid_y;
    return true;
}

const TouchBox* TouchBoxGet(u32* id, const u16 x, const u16 y, const TouchBox* tbs, const u32 tbn) {
    // check if inside touchbox
    for (u32 i = 0; !tbn || (i < tbn); i++) {
        const TouchBox* tb = tbs + i;
        if (tb->id == 0) break;
        if ((x >= tb->x) && (y >= tb->y) &&
            (x < tb->x + tb->w) && (y < tb->y + tb->h)) {
            if (id) *id = tb->id;
            return tb; // we know const is discarded here
        }
    }

    if (id) *id = 0;
    return NULL;
}

u32 InputWait(u32 timeout_sec) {
    static u64 delay = 0;
    u64 timer = timer_start();

    u32 oldpad = HID_ReadState();
    u32 oldcart = CART_STATE;
    u32 oldsd = SD_STATE;

    // enable notification LED if shell is closed
    // (this means we're waiting for user input)
    if (oldpad & SHELL_CLOSED) {
        SetNotificationLED(1000, COLOR_GREEN);
        while (HID_ReadState() & SHELL_CLOSED);
    }

    delay = delay ? 144 : 256;

    do {
        u32 newpad = HID_ReadState();

        // handle closed shell (wait for open)
        if (newpad & SHELL_CLOSED) {
            while (HID_ReadState() & SHELL_CLOSED);
            continue;
        }

        // no buttons pressed, check for I/O changes instead
        if (!(newpad & ~(SHELL_OPEN|SHELL_CLOSED))) {
            u32 state = CART_STATE;
            if (state != oldcart)
                return state ? CART_INSERT : CART_EJECT;

            state = SD_STATE;
            if (state != oldsd)
                return state ? SD_INSERT : SD_EJECT;

            oldpad = 0;
            delay = 0;
            continue;
        }

        // special case for dpad keys
        // if any of those are held, don't wait for key changes
        // but do insert a small latency to make
        // sure any menus don't go flying off
        if ((newpad == oldpad) &&
            (!(newpad & BUTTON_ARROW) ||
            (delay && (timer_msec(timer) < delay))))
            continue;

        // screenshot handling
        if ((newpad & BUTTON_ANY) == (BUTTON_R1 | BUTTON_L1))
            CreateScreenshot();

        return newpad;
    } while (!timeout_sec || (timeout_sec && (timer_sec(timer) < timeout_sec)));

    return TIMEOUT_HID;
}

bool CheckButton(u32 button) {
    return (HID_ReadState() & button) == button;
}

void ButtonToString(u32 button, char* str) {
    const char* strings[] = { BUTTON_STRINGS };

    *str = '\0';
    if (button) {
        u32 b = 0;
        for (b = 0; !((button>>b)&0x1); b++);
        if (b < countof(strings)) strcpy(str, strings[b]);
    }
}

u32 StringToButton(char* str) {
    const char* strings[] = { BUTTON_STRINGS };

    u32 b = 0;
    for (b = 0; b < countof(strings); b++)
        if (strcmp(str, strings[b]) == 0) break;

    return (b == countof(strings)) ? 0 : 1<<b;
}
