#include "gbscreen.h"
#include <cstring>
#include "sm83.h"
#include "mem.h"
#include "audio_viewer.h"
//#include "zx_ula.h"
#include <SDL2/SDL.h>
#include "ui_window.h"
#include "debug.h"
#include "ui.h"

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

    uint32_t palette[2][4] = {
        { 0xFFFFFF, 0xAAAAAA, 0x555555, 0x000000 },
        { 0xA0AA05, 0x749527, 0x487F49, 0x2E4326/*0x1D6A6B*/ }
    };
    SDL_Window *win = nullptr;
    SDL_Renderer *ren = nullptr;
    SDL_Texture *tex = nullptr;
    SDL_Texture *uitex = nullptr;

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

    uint8_t zoom = 3;
    uint8_t use_palette=1;
    bool fullscreen = false;
    bool full_refresh = true;
    int fullscreen_scale = 1;
    SDL_Rect dest_rect;

    //uint32_t time=0;

    uint32_t t_screen = 0;

    uint8_t gb_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)

    #define IF 0xff0f
    bool last_interrupt_lcd_state = false;

    bool eventHandler(SDL_Event *e)
    {
        if (e->type==SDL_WINDOWEVENT) {
            if (e->window.event==SDL_WINDOWEVENT_CLOSE) {
                return false;
            } else if ((e->window.event==SDL_WINDOWEVENT_SHOWN) || (e->window.event==SDL_WINDOWEVENT_EXPOSED)) {
                redraw();
            }
        }
        if (!debug::debugging()) {
            if (debug::paused()) {
                if (e->type == SDL_KEYDOWN) {
                    if (e->key.keysym.scancode==SDL_SCANCODE_ESCAPE) {
                        const uint8_t dt = sm83::step();
                        debug::cont();
                        gbscreen::refresh(dt);
                    }
                }
            } else {
                if (e->type == SDL_KEYDOWN) {
                    if (e->key.keysym.scancode==SDL_SCANCODE_ESCAPE) {
                        debug::pause();
                        gbscreen::redraw();
                    } else if (e->key.keysym.scancode==SDL_SCANCODE_F1) {
                        gbscreen::decZoom();
                    } else if (e->key.keysym.scancode==SDL_SCANCODE_F2) {
                        gbscreen::incZoom();
                    } else if (e->key.keysym.scancode==SDL_SCANCODE_F3) {
                        gbscreen::toggleFullscreen();
                    } else if (e->key.keysym.scancode==SDL_SCANCODE_F6) {
                        //zx_tape::play();
                    } else if (e->key.keysym.scancode==SDL_SCANCODE_F7) {
                        //zx_tape::rewind();
                    } else if (e->key.keysym.scancode==SDL_SCANCODE_Q && e->key.keysym.mod & KMOD_CTRL) {
                        return false;
                    }
                }
            }
        }
        if (e->type == SDL_MOUSEMOTION) {
            SDL_ShowCursor(true);
        }
        return true;
    }

    void reinit()
    {
        if (win) ui::window::unregisterWindow(SDL_GetWindowID(win));

        if (tex) SDL_DestroyTexture(tex);
        if (uitex) SDL_DestroyTexture(uitex);
        if (ren) SDL_DestroyRenderer(ren);
        if (win) SDL_DestroyWindow(win);

        const int z = fullscreen ? 1 : zoom;
        win = SDL_CreateWindow("Gameboy Screen", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 160*z, 144*z, fullscreen?SDL_WINDOW_FULLSCREEN_DESKTOP:SDL_WINDOW_SHOWN);
        ren = SDL_CreateRenderer(win, -1, SDL_RENDERER_ACCELERATED);
        tex = SDL_CreateTexture(ren, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, 160, 144);
        uitex = ui::createtexture(ren);

        ui::window::registerWindow(SDL_GetWindowID(win), eventHandler);

        if (fullscreen)        
        {
            int w, h;
            SDL_GetWindowSize(win, &w, &h);
            fullscreen_scale = h/144;
            dest_rect.w = 160 * fullscreen_scale;
            dest_rect.h = 144 * fullscreen_scale;
            dest_rect.x = (w - dest_rect.w)/2;
            dest_rect.y = (h - dest_rect.h)/2;
        }
        else
        {
            dest_rect.x = dest_rect.y = 0;
            dest_rect.w = 160 * zoom;
            dest_rect.h = 144 * zoom;
        }
        
        _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();

        focus();
    }

    void init(int mode)
    {
        reinit();
    }

    void focus()
    {
        if (win)
        {
            SDL_RaiseWindow(win);
            redraw();
        }
    }
    
    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) {
                    //uint8_t pixel = gb_pixels[current_pixel+LY*160];
                    gb_pixels[current_pixel+LY*160] = line_buffer[current_pixel];// > line_buffer[current_pixel] ? pixel-1 : line_buffer[current_pixel];
                }
            }

            // gestió de en quin dot i linea estem, i tot el que ha de passar
            //uint8_t interrupts = 0x00;
            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; // interrupts |= INTERRUPT_LCD;
            }
            else if (dots_in_scanline==456)
            {
                dots_in_scanline = 0; 
                LY++;
                if (LY==154) LY=0;
                if (LY==144)
                {
                    // Set vblank interrupt
                    mem::writeMem(IF, mem::readMem(IF) | INTERRUPT_VBLANK);
                    STAT = (STAT & 0xFC) | 0x01; // Set mode 1
                    //interrupts |= INTERRUPT_VBLANK;
                    if (STAT&0x10) current_interrupt_lcd_state = true; //interrupts |= INTERRUPT_LCD;
                }
                else
                {
                    if (LY<144)
                    {
                        STAT = (STAT & 0xFC) | 0x02; // Set mode 2
                        if (STAT&0x20) current_interrupt_lcd_state = true; // interrupts |= INTERRUPT_LCD;
                        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; // interrupts |= INTERRUPT_LCD;
                }
                else
                {
                    STAT = (STAT & 0xFB);
                }
            }

            if (!last_interrupt_lcd_state && current_interrupt_lcd_state)
            {
                mem::writeMem(IF, mem::readMem(IF) | INTERRUPT_LCD);
                //sm83::interrupt(interrupts);
            }
            last_interrupt_lcd_state = current_interrupt_lcd_state;

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

    void redraw(const bool present)
    {
        //if (zx_tape::getplaying() && zx_tape::getOption(ZXTAPE_OPTION_FAST_LOAD)) return;

        ui::setrenderer(ren, uitex);

        Uint32* pixels;
        int pitch;
        SDL_LockTexture(tex, NULL, (void**)&pixels, &pitch);
        for (int i=0; i<160*144;++i) *(pixels++) = palette[use_palette][gb_pixels[i]];
        SDL_UnlockTexture(tex);

        if (fullscreen)
        {
            SDL_SetRenderDrawColor(ren, 0, 0, 0, 255);
            SDL_RenderClear(ren);
        }

        // Pintem la textura a pantalla
        SDL_RenderCopy(ren, tex, NULL, &dest_rect);

        // Pintem les ralles dels pixels
        if (zoom>2 || fullscreen) {
            SDL_SetRenderDrawBlendMode(ren, SDL_BLENDMODE_BLEND);
            SDL_SetRenderDrawColor(ren, 160, 170, 5, 96);
            const float scale = fullscreen ? fullscreen_scale : zoom;
            for (int i=0;i<144;++i) SDL_RenderDrawLine(ren, dest_rect.x, dest_rect.y+i*scale, dest_rect.x+159*scale, dest_rect.y+i*scale);
            for (int i=0;i<160;++i) SDL_RenderDrawLine(ren, dest_rect.x+i*scale, dest_rect.y, dest_rect.x+i*scale, dest_rect.y+143*scale);
        }

        if (present)
            SDL_RenderPresent(ren);
        else
        {
            SDL_SetRenderDrawBlendMode(ren, SDL_BLENDMODE_BLEND);
            SDL_SetRenderDrawColor(ren, 0, 0, 0, 128);
            SDL_Rect rect {0,0,160*zoom,144*zoom};
            if (fullscreen) SDL_GetWindowSize(win, &rect.w, &rect.h);
            SDL_RenderFillRect(ren, &rect);
        }
        //audio_viewer::refresh();
    }

    void present()
    {
        SDL_RenderPresent(ren);
    }

    void setTitle(const char* title)
    {
        char tmp[256];
        strcpy(tmp, "Gameboy Screen");
        strcat(tmp, title);
        SDL_SetWindowTitle(win, tmp);
    }

    void setZoom(const int value)
    {
        if (fullscreen) return;

        if (value < 1) return;
        SDL_DisplayMode dm;
        SDL_GetCurrentDisplayMode(0, &dm);

        if (160*value > dm.w) return;
        if (144*value > dm.h) return;

        zoom = value;
        reinit();
    }

    void incZoom()
    {
        if (fullscreen) return;
        setZoom(zoom+1);
    }

    void decZoom()
    {
        if (fullscreen) return;
        setZoom(zoom-1);
    }

    void toggleFullscreen()
    {
        fullscreen = !fullscreen;
        reinit();
    }

    const bool getFullscreen()
    {
        return fullscreen;
    }
}
