#include "ppu.h"
#include "mem.h"
#include "interrupts.h"
#include "display.h"

namespace ppu
{
    struct oam_entry_t
    {
        uint8_t y, x, tile, attr;
    };

    uint32_t t_states_total = 70224;
    uint32_t t_states_per_scanline = 456;
    uint32_t vsync_lines = 10;

    uint32_t t_screen = 0;

    uint8_t pixels[160*144];

    uint16_t dots_in_scanline = 0;
    uint8_t line_buffer[160];

    uint8_t *_LCDC = nullptr;
    uint8_t *_STAT = nullptr;
    uint8_t *_SCY = nullptr;
    uint8_t *_SCX = nullptr;
    uint8_t *_LY = nullptr;
    uint8_t *_LYC = nullptr;
    uint8_t *_BGP = nullptr;
    uint8_t *_WY = nullptr;
    uint8_t *_WX = nullptr;
    uint8_t *OBP = nullptr;

    oam_entry_t *oam = nullptr;
    uint8_t *vram = nullptr;

    #define LCDC (*_LCDC)
    #define STAT (*_STAT)
    #define SCY (*_SCY)
    #define SCX (*_SCX)
    #define LY (*_LY)
    #define LYC (*_LYC)
    #define BGP (*_BGP)
    #define WY (*_WY)
    #define WX (*_WX)

    bool last_interrupt_lcd_state = false;

    void init()
    {
        _LCDC = mem::rawHram(0xff40);
        _STAT = mem::rawHram(0xff41);
        _SCY  = mem::rawHram(0xff42);
        _SCX  = mem::rawHram(0xff43);
        _LY   = mem::rawHram(0xff44);
        _LYC  = mem::rawHram(0xff45);
        _BGP  = mem::rawHram(0xff47);
        _WY   = mem::rawHram(0xff4a);
        _WX   = mem::rawHram(0xff4b);
        OBP  = mem::rawHram(0xff48);

        oam   = (oam_entry_t*)mem::rawHram(0xfe00);
        vram  = mem::rawVram();
    }

    void fill_line_buffer_bkg()
    {
        if ((LCDC & 0x1) == 0) {
            for (int i=0; i<160; ++i) line_buffer[i]=0;
            return;
        }
        const uint16_t ty = uint8_t(SCY+LY) >> 3;
        const uint8_t ly = uint8_t(SCY+LY) & 0x7;
        uint16_t tx = SCX >> 3;
        uint8_t ox = SCX & 0x7;

        uint16_t base_tilemap_address = LCDC&0x8 ? 0x1c00 : 0x1800;

        int pi = 0;
        while(true) {
            uint16_t tilemap_address = base_tilemap_address + tx + (ty<<5);
            uint16_t tile = vram[tilemap_address];
            uint16_t base_tile_address = 0x0000;
            if ( ((LCDC&0x10)==0) && (tile<128) ) base_tile_address = 0x1000;
            uint16_t tile_address = base_tile_address + (tile<<4) + (ly*2);

            uint8_t a = vram[tile_address];
            uint8_t b = vram[tile_address+1];
            for (int i=0; i<8; ++i) {
                if (ox==0) {
                    uint8_t index = (a&0x80 ? 1 : 0) + (b&0x80 ? 2 : 0 );
                    line_buffer[pi++] = (BGP >> (index*2)) & 0x3;
                } else {
                    ox--;
                }
                a=a<<1; b=b<<1;
                if (pi==160) return;
            }
            tx = (tx+1)&0x1f;
        }
    }

    void fill_line_buffer_win()
    {
        if ((LCDC & 0x21) != 0x21) return;
        if (LY<WY) return;
        const uint16_t ty = uint8_t(LY-WY) >> 3;
        const uint8_t ly = uint8_t(LY-WY) & 0x7;
        uint8_t ox = WX<7 ? 7-WX : 0;
        uint16_t tx = 0;

        uint16_t base_tilemap_address = LCDC&0x40 ? 0x1c00 : 0x1800;

        int pi = WX<7 ? 0 : WX-7;
        while(true) {
            uint16_t tilemap_address = base_tilemap_address + tx + (ty<<5);
            uint16_t tile = vram[tilemap_address];
            uint16_t base_tile_address = 0x0000;
            if ( ((LCDC&0x10)==0) && (tile<128) ) base_tile_address = 0x1000;
            uint16_t tile_address = base_tile_address + (tile<<4) + (ly*2);

            uint8_t a = vram[tile_address];
            uint8_t b = vram[tile_address+1];
            for (int i=0; i<8; ++i) {
                if (ox==0) {
                    uint8_t index = (a&0x80 ? 1 : 0) + (b&0x80 ? 2 : 0 );
                    line_buffer[pi++] = (BGP >> (index*2)) & 0x3;
                } else {
                    ox--;
                }
                a=a<<1; b=b<<1;
                if (pi==160) return;
            }
            tx = (tx+1)&0x1f;
        }
    }

    void fill_line_buffer_obj()
    {
        if ((LCDC & 0x2) == 0) return;

        const uint8_t height = (LCDC & 0x4) ? 16 : 8;
        uint8_t obj_list[10] = { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255 };
        int num_obj_found=0;
        int obj=0;
        // Revisem els 40 posibles sprites o fins a trobar 10...
        while (obj<40 && num_obj_found<10) {
            // Si el sprite està tocant la linea actual l'afegim a la llista
            if ( (LY+16 >= (oam[obj].y)) && (LY+16 < (oam[obj].y+height)) ) {
                obj_list[num_obj_found]=obj;
                num_obj_found++;
            }
            obj++;
        }
        // Pintem els sprites en el buffer de sprites
        uint8_t pixels[160];
        uint8_t x_pos[160];
        for (int i=0;i<160;++i) { pixels[i] = 255; x_pos[i] = 255; }
        obj=0;
        while (obj_list[obj] != 255) {
            oam_entry_t *o = &oam[obj_list[obj]];
            const uint8_t ly = uint8_t(LY-o->y) & 0xf;

            uint16_t tile = height==8 ? o->tile : o->tile & 0xFE; // si es dos tiles de alt, el primer sempre comença en numero parell
            uint8_t yflip = o->attr&0x40 ? (height-1)-ly : ly; // està invertit verticalment?
            uint16_t tile_address = 0x0000 + (tile<<4) + (yflip*2);

            uint8_t a = vram[tile_address];
            uint8_t b = vram[tile_address+1];

            for (int i=0; i<8; ++i) { // Per a cada pixel de la linea del tile...
                if (o->x+i>=168) break; // Si ja estem fora de la pantalla per la dreta, eixim del bucle
                if (o->x+i>=8) { // Si està dins de la pantalla...
                    if (x_pos[o->x+i-8]>o->x) { // Si te una x menor que la que tenía
                        //uint8_t xflip = o->attr&0x20 ? 8 : 0; // està invertit horitzontalment?
                        const uint8_t ppos = 1 << ( o->attr&0x20 ? i : 7-i);
                        const uint8_t val = (a&ppos ? 1 : 0) + (b&ppos ? 2 : 0 ); // agafem el pixel que toca
                        if (val) {  // Si el pixel no es transparent...
                            const uint8_t color = (OBP[(o->attr>>4)&1] >> (val*2)) & 0x3;
                            pixels[o->x+i-8] = color | o->attr&0x80;; // el pintem al buffer, amb el flag de prioritat respecte al BKG
                            x_pos[o->x+i-8] = o->x; // I apuntem la seua x per a comparar després
                        }
                    }
                }  
            }
            obj++;
        }
        // Per últim, volquem els pixels que toque al buffer de linea
        for (int i=0; i<160; ++i) {
            if (pixels[i]!=255) { // si el pixel no es transparent...
                if ( !(pixels[i]&0x80) || (line_buffer[i]==0) ) { // Si te prioritat o el color de fondo es 0...
                    line_buffer[i] = pixels[i]&0x03; // pintem el pixel (llevant el flag de prioritat)
                }
            }
        }
    }

    void refresh(const uint32_t dt, const bool full)
    {
        if ((LCDC&0x80)==0) return;

        for (int i=0;i<dt;++i)
        {
            // Açò va volcant els pixels del line_buffer en pantalla
            if ( (STAT&0x3)==3) {
                uint16_t current_pixel = dots_in_scanline-80;
                if (current_pixel<160) {
                    pixels[current_pixel+LY*160] = line_buffer[current_pixel];
                }
            }

            // gestió de en quin dot i linea estem, i tot el que ha de passar
            bool current_interrupt_lcd_state = false;
            dots_in_scanline++;
            if ( (dots_in_scanline==80) && (LY<144) )
            {
                STAT = (STAT & 0xFC) | 0x3; // Set mode 3
            }
            else if ( (dots_in_scanline==252) && (LY<144) )
            {
                STAT = (STAT & 0xFC); // Set mode 0
                if (STAT&0x08) current_interrupt_lcd_state = true;
            }
            else if (dots_in_scanline==456)
            {
                dots_in_scanline = 0; 
                LY++;
                if (LY==154) LY=0;
                if (LY==144)
                {
                    // Set vblank interrupt
                    mem::requestInterrupt(gb::interrupts::VBLANK);
                    STAT = (STAT & 0xFC) | 0x01; // Set mode 1
                    if (STAT&0x10) current_interrupt_lcd_state = true;
                }
                else
                {
                    if (LY<144)
                    {
                        STAT = (STAT & 0xFC) | 0x02; // Set mode 2
                        if (STAT&0x20) current_interrupt_lcd_state = true;
                        fill_line_buffer_bkg();
                        fill_line_buffer_win();
                        fill_line_buffer_obj();
                    }
                }
                if (LY==LYC)
                {
                    STAT = (STAT & 0xFB) | 0x04;
                    if (STAT&0x40) current_interrupt_lcd_state = true;
                }
                else
                {
                    STAT = (STAT & 0xFB);
                }
            }

            if (!last_interrupt_lcd_state && current_interrupt_lcd_state)
            {
                mem::requestInterrupt(gb::interrupts::LCD);
            }
            last_interrupt_lcd_state = current_interrupt_lcd_state;

            t_screen++;
            if (t_screen>=t_states_total)
            {
                t_screen-=t_states_total;
                display::redraw();
            }
        }
    }

    const uint8_t *getpixels()
    {
        return pixels;
    }
}
