#ifndef EMUDORE_IO_H
#define EMUDORE_IO_H

#include <lib/stdint.h>
#include <lib/vga.h>
#include <c64/diagfont.h>   // 5x7 glyph font for the HDGL projection surface
#include <c64/cpu.h>
#include <c64/memory.h>

#include <drivers/keyscancodes.h>
#include <drivers/ata.h>
#include <filesystem/fat.h>
#include <drivers/serial.h>
#include <drivers/rtc.h>

//#define _NO_BORDER_

#ifndef _NO_BORDER_
#define VIRT_WIDTH 403
#define VIRT_HEIGHT 284
#else
#define VIRT_WIDTH 320
#define VIRT_HEIGHT 200
#endif

// current milliseconds since epoch
extern uint32_t current_milli;

using namespace myos::hardwarecommunication;
using namespace myos::filesystem;
using namespace myos::drivers;

/**
 * @brief IO devices
 *
 * This class implements Input/Output devices connected to the 
 * Commodore 64 such as the screen and keyboard.
 */
class IO
{
  private:
    Cpu *cpu_;
    Memory *mem_;
    uint32_t *frame_;
    size_t cols_;
    size_t rows_;
    
    uint8_t keyboard_matrix_[8];
    bool retval_;
    /* keyboard mappings */
    enum kKeyEvent
    {
      kPress,
      kRelease,
    };
    uint8_t shift;
    uint8_t mode;
    uint8_t joy1, joy2;

    static const uint8_t kbd[8][8];
  
    unsigned int next_key_event_at_;
    static const int kWait = 18000;
    /* vertical refresh sync */
    void vsync();
        
    Fat32 *fat32_;
    void file_load();
    void file_save();
    void file_open();
    void file_close();
    void file_get();
    
    SerialDriver *serial_;
    RTCDriver *rtc_;

    uint16_t *vscreen_; 		// pointer to the offset of virtual screen.
    uint16_t *pscreen_;
    double scaleWidth;
    double scaleHeight;
    
    uint8_t *vgaMem_;
    uint32_t screen_width_;
    uint32_t screen_height_;
    uint32_t screen_pitch_;
    uint8_t screen_bpp_;
    uint8_t pixel_width_;
    
    uint32_t prev_frame_milli;

public:
    IO();
    ~IO();
    void init_display(uint32_t* vgaMemAddress, uint32_t width, uint32_t height, uint32_t pitch, uint8_t bpp);

    bool emulate();
    void process_events();
    void cpu(Cpu *v){cpu_=v;};
    void memory(Memory *m) {mem_ = m;};
    void fat32(Fat32 *m) { fat32_ = m; };
    void serial(SerialDriver *m) { serial_ = m; };
    void rtc(RTCDriver *m) { rtc_ = m; };
    
    uint8_t SkipFrames = 0;
    bool step = false;
    uint16_t color_palette[16];
    
    void init_color_palette();
    void init_keyboard();
    void OnKeyDown(uint8_t c);
    void OnKeyUp(uint8_t c);
    uint8_t getJoystick(uint8_t num);
    void SendSerial(uint8_t c);
    
    void type_character(char c);
    inline uint8_t keyboard_matrix_row(int col){return keyboard_matrix_[col];};
    
    inline void put_pixel(int x,int y, int color) {
      uint8_t *pixel = vgaMem_ + y*screen_pitch_ + x*pixel_width_;     
      *pixel = color_palette[color] & 255;              
      *(pixel + 1) = (color_palette[color] >> 8) & 255;
    };
    
    inline void screen_update_pixel(int x, int y, int color) { 
      *(vscreen_ + y * VIRT_WIDTH  + x) = color;
    };
    
    inline void screen_draw_rect(int x, int y, int n, int color) {
      for(int i=1; i <= n ; i++)
	*(vscreen_ + y * VIRT_WIDTH + x + i) = color;
    };
    
    inline void screen_draw_border(int y, int color) {
      for(int i=0; i < cols_ ; i++)
	*(vscreen_ + y * VIRT_WIDTH  + i) = color;
    };
    
    inline void screen_refresh() {

      static uint8_t skipCtr = 0;
      
      if(SkipFrames == 0 || skipCtr == SkipFrames)
      {
	// ── BOOT LANDMARK DEBUG (disabled) ──
	if(false)
	{
	  extern volatile unsigned int g_lm_reset,g_lm_ramtas_end,g_lm_restor,
	                               g_zp_c2,g_zp_max_c2,
	                               g_lm_basic,g_lm_cartjmp,g_pc_now;
	  const char* hx="0123456789ABCDEF";
	  char line[40]; int di=0;
	  auto put2=[&](unsigned int v){ line[di++]=hx[(v>>4)&0xF]; line[di++]=hx[v&0xF]; };
	  line[di++]='R'; line[di++]='S'; line[di++]='T'; line[di++]=':'; put2(g_lm_reset);
	  line[di++]=' '; line[di++]='R'; line[di++]='A'; line[di++]=':'; put2(g_lm_ramtas_end);
	  line[di++]=' '; line[di++]='B'; line[di++]='A'; line[di++]=':'; put2(g_lm_basic);
	  line[di++]=' '; line[di++]='C'; line[di++]='T'; line[di++]=':'; put2(g_lm_cartjmp);
	  line[di++]=' '; line[di++]='C'; line[di++]='2'; line[di++]=':'; put2(g_zp_c2);
	  line[di++]=' '; line[di++]='M'; line[di++]='X'; line[di++]=':'; put2(g_zp_max_c2);
	  line[di++]=' '; line[di++]='P'; line[di++]='C'; line[di++]=':';
	  line[di++]=hx[(g_pc_now>>12)&0xF]; line[di++]=hx[(g_pc_now>>8)&0xF];
	  line[di++]=hx[(g_pc_now>>4)&0xF]; line[di++]=hx[g_pc_now&0xF];
	  line[di]=0;
	  // clear a strip and render
	  for(uint32_t y=0;y<40;y++){uint8_t*row=vgaMem_+y*screen_pitch_;for(uint32_t x=0;x<screen_width_;x++){row[x*2]=0;row[x*2+1]=0;}}
	  const int SC=2; const int GW=6*SC; const uint16_t FG=0x07E0;
	  for(int i=0;line[i];i++){
	    const unsigned char* g=font5x7[(line[i]<32||line[i]>127)?0:line[i]-32];
	    unsigned int ox=8+i*GW, oy=8;
	    for(int r=0;r<7;r++){unsigned char bits=g[r];
	      for(int cc=0;cc<5;cc++) if(bits&(0x10>>cc))
		for(int sy=0;sy<SC;sy++)for(int sx=0;sx<SC;sx++){
		  unsigned int px=ox+cc*SC+sx,py=oy+r*SC+sy;
		  if(px<screen_width_&&py<screen_height_){uint8_t*p=vgaMem_+py*screen_pitch_+px*2;p[0]=FG&255;p[1]=(FG>>8)&255;}
		}
	    }
	  }
	}

	// ── HDGL PROJECTION SURFACE ─────────────────────────────────────
	// The face is a moiré of the private instance, not a readout of it.
	// CHROUT (running on the real 6502/ROMs) modulates g_face_screen; we
	// render that surface here with our own glyph font. The private C64
	// memory is never read. law shared, nothing sent.
	extern volatile unsigned char g_face_screen[40*25];
	extern volatile unsigned int  g_face_active;
	if(g_face_active)
	{
	  const unsigned int COLS=40, ROWS=25;
	  const int SC=2;                        // glyph pixel scale
	  const int GW=6*SC, GH=9*SC;            // cell size on the framebuffer
	  // ── C64 16-colour palette -> RGB565 (observed VIC state drives colour) ──
	  static const uint16_t c64pal[16] = {
	    0x0000, 0xFFFF, 0x8000, 0x07FF,   // black white red cyan
	    0x8010, 0x0400, 0x0010, 0xFFE0,   // purple green blue yellow
	    0xC200, 0x6100, 0xC410, 0x39E7,   // orange brown lt-red dk-grey
	    0x7BEF, 0x87F0, 0x841F, 0xAD55    // grey lt-green lt-blue lt-grey
	  };
	  extern volatile unsigned char g_vic_border, g_vic_backgnd;
	  extern volatile unsigned char g_face_color[40*25];
	  const uint16_t BORDER = c64pal[g_vic_border  & 0x0F];
	  const uint16_t BG     = c64pal[g_vic_backgnd & 0x0F];
	  // text area geometry — CENTERED so the border is a uniform frame around
	  // it (like a real C64), not a small box in the top-left of a big border.
	  const unsigned int TW = COLS*GW, TH = ROWS*GH;    // text area size
	  const unsigned int TX = (screen_width_  > TW) ? (screen_width_  - TW)/2 : 24;
	  const unsigned int TY = (screen_height_ > TH) ? (screen_height_ - TH)/2 : 24;
	  const unsigned int MARGIN = 16;                   // border frame width
	  const unsigned int BX0 = (TX>MARGIN)?TX-MARGIN:0;
	  const unsigned int BY0 = (TY>MARGIN)?TY-MARGIN:0;
	  const unsigned int BX1 = TX+TW+MARGIN;
	  const unsigned int BY1 = TY+TH+MARGIN;
	  for(uint32_t y=0;y<screen_height_;y++){
	    uint8_t* row=vgaMem_+y*screen_pitch_;
	    bool yin = (y>=BY0 && y<BY1);
	    for(uint32_t x=0;x<screen_width_;x++){
	      uint16_t col = (yin && x>=BX0 && x<BX1) ? BG : BORDER;
	      row[x*2]=col&255; row[x*2+1]=(col>>8)&255;
	    }
	  }
	  // draw each cell's glyph (scaled) in its observed per-cell colour ($D800)
	  for(unsigned int cy=0; cy<ROWS; cy++)
	  for(unsigned int cx=0; cx<COLS; cx++)
	  {
	    unsigned char ch = g_face_screen[cy*COLS+cx];
	    if(ch<32||ch>127) ch=' ';
	    const unsigned char* g = font5x7[ch-32];
	    uint16_t FG = c64pal[g_face_color[cy*COLS+cx] & 0x0F];
	    unsigned int ox = TX + cx*GW;
	    unsigned int oy = TY + cy*GH;
	    for(int r=0;r<7;r++){
	      unsigned char bits=g[r];
	      for(int c=0;c<5;c++){
		if(bits & (0x10>>c)){
		  for(int sy=0;sy<SC;sy++) for(int sx=0;sx<SC;sx++){
		    unsigned int px=ox+c*SC+sx, py=oy+r*SC+sy;
		    if(px<screen_width_ && py<screen_height_){
		      uint8_t* p=vgaMem_+py*screen_pitch_+px*2;
		      p[0]=FG&255; p[1]=(FG>>8)&255;
		    }
		  }
		}
	      }
	    }
	  }
	  sync();
	  skipCtr=0;
	  return;                                // face drawn; skip VIC blit
	}

	// DIAG: detect whether the VIC has written anything into vscreen_.
	if(false)
	{
	  extern volatile unsigned int g_bad_op, g_bad_pc, g_ran_ops;
	  static volatile unsigned int frame_ctr=0; frame_ctr++;
	  bool any=false;
	  uint32_t lim = VIRT_WIDTH*VIRT_HEIGHT;
	  for(uint32_t i=0;i<lim;i++){ if(vscreen_[i]){any=true;break;} }
	  uint16_t m1 = any ? 0x07E0 : 0xF800;
	  uint16_t m2 = (g_ran_ops>1000) ? 0x07E0 : 0xF800;
	  uint16_t m3 = (g_bad_op!=0xFFFF) ? 0xFFFF : 0x001F;
	  // block4: brightness scales with frame count (blue if frames advancing)
	  uint16_t m4 = (frame_ctr>3) ? 0x07FF : 0x0000;
	  extern volatile unsigned int g_vis_lines;
	  extern volatile unsigned int g_pc_min, g_pc_max, g_pc_hit_e000;
	  uint16_t m5 = (g_vis_lines>0) ? 0x07E0 : 0xF800;
	  // block6: green if 6502 PC ever reached KERNAL ROM ($E000+), else red
	  uint16_t m6 = (g_pc_hit_e000) ? 0x07E0 : 0xF800;
	  // block7: green if PC span is wide (>0x0800 => traversing code), else red (stuck)
	  uint16_t m7 = ((g_pc_max - g_pc_min) > 0x0800) ? 0x07E0 : 0xF800;
	  extern volatile unsigned int g_border_writes, g_border_val;
	  // block8: green if KERNAL ever wrote the VIC border register ($D020)
	  uint16_t m8 = (g_border_writes>0) ? 0x07E0 : 0xF800;
	  // block9: encode current border color value as grayscale-ish (0=black)
	  uint16_t m9 = (g_border_val!=0) ? 0x07E0 : 0xF800;
	  extern volatile unsigned int g_pc_hit_e518, g_pc_past_ramtas;
	  uint16_t mA = (g_pc_past_ramtas) ? 0x07E0 : 0xF800; // got past RAM test
	  uint16_t mB = (g_pc_hit_e518) ? 0x07E0 : 0xF800;    // reached screen init
	  uint16_t marks[11]={m1,m2,m3,m4,m5,m6,m7,m8,m9,mA,mB}; int xo[11]={0,25,50,75,100,125,150,175,200,225,250};
	  for(int b=0;b<11;b++)
	    for(uint32_t yy=0; yy<20; yy++)
	      for(uint32_t xx=0; xx<20; xx++)
	      { uint8_t* p=vgaMem_+yy*screen_pitch_+(xo[b]+xx)*2; p[0]=marks[b]&255; p[1]=(marks[b]>>8)&255; }
	  // Paint the live 6502 PC (g_pc_last) as 16 bit-cells on row y=30:
	  // white cell = 1, dark cell = 0, MSB left. Lets us read the stuck PC.
	  {
	    extern volatile unsigned int g_pc_last;
	    unsigned int v = g_pc_last;
	    for(int bit=0; bit<16; bit++)
	    {
	      uint16_t col = (v & (0x8000>>bit)) ? 0xFFFF : 0x2104;
	      for(uint32_t yy=30; yy<48; yy++)
		for(uint32_t xx=0; xx<16; xx++)
		{ uint8_t* p=vgaMem_+yy*screen_pitch_+(bit*18+xx)*2; p[0]=col&255; p[1]=(col>>8)&255; }
	    }
	  }
	}

	// Upscale the C64 virtual screen (VIRT_WIDTH x VIRT_HEIGHT, palette
	// indices) into pscreen_ (screen_width_ x screen_height_, palette
	// indices), nearest-neighbour.
	for(uint16_t cy = 0; cy < screen_height_; cy++)
	  for(uint16_t cx = 0; cx < screen_width_; cx++)
	  {
	    uint32_t sy = (uint16_t)(cy / scaleHeight);
	    uint32_t sx = (uint16_t)(cx / scaleWidth);
	    uint32_t nearestMatch = sy * VIRT_WIDTH + sx;
	    pscreen_[cy * screen_width_ + cx] = vscreen_[nearestMatch];
	  }

	// Blit pscreen_ (one uint16 palette index per pixel) into the linear
	// framebuffer, honouring the byte pitch for the row stride.
	for(uint32_t y=0; y<screen_height_; y++)
	{
	  uint8_t* dstRow = vgaMem_ + y * screen_pitch_;
	  uint16_t* srcRow = pscreen_ + y * screen_width_;
	  for(uint32_t x=0; x<screen_width_; x++)
	  {
	    uint16_t color = color_palette[srcRow[x]];
	    dstRow[x*2]   = color & 255;
	    dstRow[x*2+1] = (color >> 8) & 255;
	  }
	}

	sync();
	skipCtr = 0;
      }     
      
      	if(SkipFrames > 0)
	  skipCtr++;
	else
	  skipCtr = 0;
      
      // no scaling
      /*
      for(uint16_t y=0;y<284;y++)
	for(uint16_t x=0;x<403;x++)
	  put_pixel(x,y,vscreen_[y*403+x]);*/
    };
    
    inline void sync()
    {
      // Frame limiter. Guard against unsigned underflow: if we've already
      // spent as long as (or longer than) a frame, don't wait at all.
      uint32_t current = current_milli;
      uint32_t t = current - prev_frame_milli;
      const uint32_t frame_ms = 19; // ~50Hz
      if(t < frame_ms)
        delay(frame_ms - t);
      prev_frame_milli = current_milli;
    }

    inline void delay(uint32_t milliseconds)
    {
      uint32_t startmilli = current_milli;
      // Bounded wait so a stalled timer can never freeze the emulator.
      uint32_t guard = 0;
      while(current_milli < startmilli + milliseconds)
      {
        if(++guard > 200000000u) break;
      }
    }
    
    
};

#endif