#include "ultra64/leo.h"
#include "ultra64/leoappli.h"
#include "ultra64/leodrive.h"
#include "attributes.h"

#include "ultra64.h"

typedef struct {
    /* 0x0 */ u8 year;
    /* 0x1 */ u8 month;
    /* 0x2 */ u8 day;
    /* 0x3 */ u8 hour;
    /* 0x4 */ u8 minute;
    /* 0x5 */ u8 second;
} __LOCTime;

u8 __locReadTimer(__LOCTime* time);
u8 __locSetTimer(__LOCTime* time);

static const u8 ymdupper[6] = { 99, 12, 31, 23, 59, 59 };
static const u8 dayupper[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };

void leoReadTimer(void) {
    u8* rdparam;
    u8 data[4];
    u8 sense_code;
    __LOCTime time;

    sense_code = __locReadTimer(&time);
    LEOcur_command->data.time.yearlo = time.year;
    LEOcur_command->data.time.month = time.month;
    LEOcur_command->data.time.day = time.day;
    LEOcur_command->data.time.hour = time.hour;
    LEOcur_command->data.time.minute = time.minute;
    LEOcur_command->data.time.second = time.second;
    if (sense_code != 0) {
        LEOcur_command->header.sense = sense_code;
        LEOcur_command->header.status = LEO_STATUS_CHECK_CONDITION;
        return;
    }
    if ((u8)LEOcur_command->data.time.yearlo >= 0x96U) {
        LEOcur_command->data.time.yearhi = 0x19;
    } else {
        LEOcur_command->data.time.yearhi = 0x20;
    }
    LEOcur_command->header.status = 0;
}

void leoSetTimer(void) {
    LEOCmdReadTimer rd_timer;
    u8* p_tmp = &LEOcur_command->data.time.yearlo;
    u32 year;
    u32 month;
    u32 temp;
    u32 ymd;
    u8 result;
    __LOCTime time;

    // Verify values (if they're correct BCD or within limits)

    for (ymd = 0; ymd < 6; ymd++) {
        temp = *p_tmp;

        // Verify right nibble (only right nibble for some reason)
        if ((temp & 0xF) > 9) {
            // nibble is above 0x9 therefore the BCD value is invalid
            LEOcur_command->header.sense = LEO_SENSE_ILLEGAL_TIMER_VALUE;
            LEOcur_command->header.status = LEO_STATUS_CHECK_CONDITION;
            return;
        }

        // Convert BCD value to binary value
        temp = temp - ((temp >> 4) * 6);

        switch (ymd) {
            case 2:
                // Day value check
                if (dayupper[month] < temp && (temp != 0x1D || (year & 3))) {
                    LEOcur_command->header.sense = LEO_SENSE_ILLEGAL_TIMER_VALUE;
                    LEOcur_command->header.status = LEO_STATUS_CHECK_CONDITION;
                    return;
                }
                FALLTHROUGH;
            case 1:
                // Month value cannot be 0
                if (temp == 0) {
                    LEOcur_command->header.sense = LEO_SENSE_ILLEGAL_TIMER_VALUE;
                    LEOcur_command->header.status = LEO_STATUS_CHECK_CONDITION;
                    return;
                }
                FALLTHROUGH;
            default:
                // Verify max value of each time info
                if (ymdupper[ymd] < temp) {
                    LEOcur_command->header.sense = LEO_SENSE_ILLEGAL_TIMER_VALUE;
                    LEOcur_command->header.status = LEO_STATUS_CHECK_CONDITION;
                    return;
                }
        }

        year = month;
        month = temp;
        p_tmp++;
    }

    // Every value has been ymd good, now set the values in hardware

    // Prepare the time info to use
    time.year = LEOcur_command->data.time.yearlo;
    time.month = LEOcur_command->data.time.month;
    time.day = LEOcur_command->data.time.day;
    time.hour = LEOcur_command->data.time.hour;
    time.minute = LEOcur_command->data.time.minute;
    time.second = LEOcur_command->data.time.second;

    // Set the new time
    result = __locSetTimer(&time);
    if (result != 0) {
        LEOcur_command->header.sense = result;
        LEOcur_command->header.status = LEO_STATUS_CHECK_CONDITION;
        return;
    }
    // Get the time
    result = __locReadTimer(&time);
    if (result != 0) {
        LEOcur_command->header.sense = result;
        LEOcur_command->header.status = LEO_STATUS_CHECK_CONDITION;
        return;
    }
    // Check if the time is set correctly
    if (time.year != LEOcur_command->data.time.yearlo || time.month != LEOcur_command->data.time.month ||
        time.day != LEOcur_command->data.time.day || time.hour != LEOcur_command->data.time.hour ||
        time.minute != LEOcur_command->data.time.minute || time.second != LEOcur_command->data.time.second) {
        LEOcur_command->header.sense = LEO_SENSE_ILLEGAL_TIMER_VALUE;
        LEOcur_command->header.status = LEO_STATUS_CHECK_CONDITION;
        return;
    }
    LEOcur_command->header.status = LEO_STATUS_GOOD;
}

// static
u8 __locReadTimer(__LOCTime* time) {
    u32 data;
    u8 sense_code;

    sense_code = leoSend_asic_cmd_w_nochkDiskChange(0x140000, 0U);
    if (sense_code != 0) {
        return sense_code;
    }
    osEPiReadIo(LEOPiInfo, ASIC_DATA, &data);
    time->minute = (u8)((u32)(data & 0xFF000000) >> 0x18);
    time->second = (s8)((u32)(data & 0xFF0000) >> 0x10);
    sense_code = leoSend_asic_cmd_w_nochkDiskChange(0x130000, 0U);
    if (sense_code != 0) {
        time->minute = (u8)(time->minute & 0xFF7F);
        return sense_code;
    }
    osEPiReadIo(LEOPiInfo, ASIC_DATA, &data);
    time->day = (s8)((u32)(data & 0xFF000000) >> 0x18);
    time->hour = (s8)((u32)(data & 0xFF0000) >> 0x10);
    sense_code = leoSend_asic_cmd_w_nochkDiskChange(0x120000, 0U);
    if (sense_code != 0) {
        time->minute = (u8)(time->minute & 0xFF7F);
        return sense_code;
    }
    osEPiReadIo(LEOPiInfo, ASIC_DATA, &data);
    sense_code = time->minute;
    time->year = (s8)((u32)(data & 0xFF000000) >> 0x18);
    time->month = (s8)((u32)(data & 0xFF0000) >> 0x10);
    if (sense_code & 0x80) {
        time->minute = (u8)(sense_code & 0xFF7F);
        return 5;
    }
    return 0;
}

// static
u8 __locSetTimer(__LOCTime* time) {
    u32 YearMonthX10000h;
    u32 DayHourX10000h;
    u32 MinuteSecondX10000h;
    u8 result;

    YearMonthX10000h = (time->year << 0x18) + (time->month << 0x10);
    DayHourX10000h = (time->day << 0x18) + (time->hour << 0x10);
    MinuteSecondX10000h = (time->minute << 0x18) + (time->second << 0x10);
    if ((result = leoSend_asic_cmd_w_nochkDiskChange(0xF0000, YearMonthX10000h)) != 0 ||
        (result = leoSend_asic_cmd_w_nochkDiskChange(0x100000, DayHourX10000h)) != 0 ||
        (result = leoSend_asic_cmd_w_nochkDiskChange(0x110000, MinuteSecondX10000h)) != 0) {
        return result;
    }
    return 0;
}
