#include <ppu.h>
#include <lcd.h>
#include <cpu.h>
#include <interrupts.h>
#include <string.h>
#include <cart.h>

void pipeline_fifo_reset();
void pipeline_process();

void increment_ly() {
    if (window_visible() && lcd_get_context()->ly >= lcd_get_context()->win_y && lcd_get_context()->ly < lcd_get_context()->win_y + YRES) {
        ppu_get_context()->window_line++;
    }

    lcd_get_context()->ly++;

    if(lcd_get_context()->ly == lcd_get_context()->ly_compare) {
        LCDS_LYC_SET(1);
        if(LCDS_STAT_INT(SS_LYC)) {
            cpu_request_interrupt(IT_LCD_STAT);
        }
    } else {
        LCDS_LYC_SET(0);
    }
}

void load_line_sprites() {
    int cur_y = lcd_get_context()->ly;

    u8 sprite_height = LCDC_OBJ_HEIGHT;
    memset(ppu_get_context()->line_entry_array, 0, sizeof(ppu_get_context()->line_entry_array));
    for (int i=0; i<40; i++) {
        oam_entry e = ppu_get_context()->oam_ram[i];
        if (!e.x) {
            //x = 0 means not visible
            continue;
        }

        if (ppu_get_context()->line_sprite_count >= 10) {
            //max of 10 sprites per line
            break;
        }

        if(e.y <= cur_y + 16 && e.y + sprite_height > cur_y + 16) {
            //this sprite is on the current line
            oam_line_entry *entry = &ppu_get_context()->line_entry_array[
                ppu_get_context()->line_sprite_count++
            ];

            entry->entry = e;
            entry->next = NULL;

            if(!ppu_get_context()->line_sprites ||
                    ppu_get_context()->line_sprites->entry.x > e.x) {
                entry->next = ppu_get_context()->line_sprites;
                ppu_get_context()->line_sprites = entry;
                continue;
            }

            //do some sorting

            oam_line_entry *le = ppu_get_context()->line_sprites;
            oam_line_entry *prev = le;

            while(le) {
                if (le->entry.x > e.x) {
                    prev->next = entry;
                    entry->next = le;
                    break;
                }

                if(!le->next) {
                    le->next = entry;
                    break;
                }

                prev = le;
                le = le->next;
            }
        }
    }
}

void ppu_mode_oam() {
    if(ppu_get_context()->line_ticks >= 80) {
        LCDS_MODE_SET(MODE_XFER);

        ppu_get_context()->pfc.cur_fetch_state = FS_TILE;
        ppu_get_context()->pfc.line_x = 0;
        ppu_get_context()->pfc.fetch_x = 0;
        ppu_get_context()->pfc.pushed_x = 0;
        ppu_get_context()->pfc.fifo_x = 0;
    }

    if(ppu_get_context()->line_ticks == 1) {
        //read oam on the first tick only...
        ppu_get_context()->line_sprites = 0;
        ppu_get_context()->line_sprite_count = 0;

        load_line_sprites();
    }
}

void ppu_mode_xfer() {
    pipeline_process();
    if(ppu_get_context()->line_ticks == 81) {
        ppu_get_context()->pfc.lo_scroll_x = lcd_get_context()->scroll_x & 0b111;
    }

    if(ppu_get_context()->pfc.pushed_x >= XRES) {
        pipeline_fifo_reset();
        LCDS_MODE_SET(MODE_HBLANK);

        if(LCDS_STAT_INT(SS_HBLANK)) {
            cpu_request_interrupt(IT_LCD_STAT);
            printf("hblank!\n");
        }
        ppu_get_context()->rendering_window = false;
    }
}

void ppu_mode_vblank() {
    if(ppu_get_context()->line_ticks >= TICKS_PER_LINE) {
        increment_ly();
        if(lcd_get_context()->ly == LINES_PER_FRAME-1) {
            if(lcd_get_context()->ly_compare == 0) {
                LCDS_LYC_SET(1);
                if(LCDS_STAT_INT(SS_LYC)) {
                    cpu_request_interrupt(IT_LCD_STAT);
                }
            } else {
                LCDS_LYC_SET(0);
            }
        }
        if(lcd_get_context()->ly >= LINES_PER_FRAME) {
            LCDS_MODE_SET(MODE_OAM);
            if(LCDS_STAT_INT(SS_OAM)) {
                cpu_request_interrupt(IT_LCD_STAT);
            }
            lcd_get_context()->ly = 0;
            ppu_get_context()->window_line = 0;
        }

        ppu_get_context()->line_ticks = 0;
    }
}

static long prev_frame_time = 0;
static long start_timer = 0;
static long frame_count = 0;

void ppu_mode_hblank() {
    if (ppu_get_context()->line_ticks >= TICKS_PER_LINE) {
        increment_ly();

        if(lcd_get_context()->ly >= YRES) {
            LCDS_MODE_SET(MODE_VBLANK);

            cpu_request_interrupt(IT_VBLANK);

            if(LCDS_STAT_INT(SS_VBLANK)) {
                cpu_request_interrupt(IT_LCD_STAT);
            }

            ppu_get_context()->current_frame++;

            //calc FPS...
            u32 end = get_ticks();
            u32 frame_time = end - prev_frame_time;

            if(frame_time < ppu_get_context()->target_frame_time) {
                delay((ppu_get_context()->target_frame_time - frame_time));
            } else if(frame_time > ppu_get_context()->target_frame_time) {
                //printf("Frame timing missed!\n");
            }

            while(ppu_get_context()->paused) {
                delay(10);
                if(ppu_get_context()->frame){
                    ppu_get_context()->frame = false;
                    break;
                }
            }

            if (end - start_timer >= 1000) {
                u32 fps = frame_count;
                start_timer = end;
                frame_count = 0;
                //printf("FPS: %ld\n", fps);
                if(cart_need_save()){
                    cart_battery_save();
                }
            }

            frame_count++;
            prev_frame_time = get_ticks();


        } else {
            LCDS_MODE_SET(MODE_OAM);
            if(LCDS_STAT_INT(SS_OAM)) {
                cpu_request_interrupt(IT_LCD_STAT);
            }
        }

        ppu_get_context()->line_ticks = 0;
    }
}
