/*	Copyright  (c)	Günter Woigk 2012 - 2012
					mailto:kio@little-bat.de

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

	Permission to use, copy, modify, distribute, and sell this software and
	its documentation for any purpose is hereby granted without fee, provided
	that the above copyright notice appear in all copies and that both that
	copyright notice and this permission notice appear in supporting
	documentation, and that the name of the copyright holder not be used
	in advertising or publicity pertaining to distribution of the software
	without specific, written prior permission.  The copyright holder makes no
	representations about the suitability of this software for any purpose.
	It is provided "as is" without express or implied warranty.

	THE COPYRIGHT HOLDER DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
	INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO
	EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY SPECIAL, INDIRECT OR
	CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE,
	DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
	TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
	PERFORMANCE OF THIS SOFTWARE.
*/


#define LOG 1
#define SAFE 3
#include "MemoryAccessInspector.h"
#include "MemoryInspector.h"
#include <QImage>
#include <QPushButton>
#include "SimpleTerminal.h"
#include <QComboBox>
#include <QBoxLayout>
#include <QWidget>
#include <QCheckBox>
#include <QTimer>
#include "Qt/MyLineEdit.h"
#include "Machine.h"
#include "Z80/Z80.h"
#include <templates/Array.h>
#include <QMouseEvent>
#include <QToolTip>
#include <Application.h>
#include <QCursor>
#include "MachineController.h"
#include "Qt/Settings.h"


// offset mouse pointer hotspot -> 'feeled' hotspot
#define mouse_x_offset	-1
#define mouse_y_offset	-2



// ==================================================================================
// ============================   Graphics Widget   =================================
// ==================================================================================


class GWidgetRGB : public QWidget
{
public:
	QImage*			canvas;
	int				x,y,w;		// highlight position

protected:
	virtual void	resizeEvent(QResizeEvent*);
    virtual void	paintEvent(QPaintEvent*);

public:
	GWidgetRGB(QWidget*parent)		:QWidget(parent),canvas(NULL),w(1){}
	~GWidgetRGB()					{ delete canvas; }
};

void GWidgetRGB::resizeEvent(QResizeEvent*)
{
	delete canvas;
	canvas = new QImage(size(),QImage::Format_RGB32);		// pixel = 0xffRRGGBB
	update();
}

void GWidgetRGB::paintEvent(QPaintEvent*)
{
	if(!canvas) { LogLine("GWidgetRGB::paintEvent() called with canvas==NULL"); return; }
	QPainter p(this);
	p.drawImage(0,0,*canvas);

	if(x>=0 && y>=0 && x<width() && y<height())
	{
		p.setPen(Qt::gray);
		p.drawRect(x/w*w-1,y/w*w-1,w,w);
	}
}


// ==================================================================================
// =======================   MemoryAccess Inspector   ===============================
// ==================================================================================



MemoryAccessInspector::MemoryAccessInspector(QWidget* parent, IsaObject *item)
:	MemoryInspector(parent,item,MemAccess)
{
	LogIn("new MemoryAccessInspector");

	XXXASSERT(cpu_r_access==1<<16);
	XXXASSERT(cpu_w_access==1<<17);
	XXXASSERT(cpu_x_access==1<<18);
	XXXASSERT(machine!=NULL);
	XXXASSERT(item!=NULL);

/* setup by base class MemoryInspector:
	data_source
	data_start
	data_size
	display_base_address
	ram_page
	rom_page
	bytes_per_row
	rows
	this.size
*/
	mode  = settings.value(key_memoryview_access_decaymode,modeDecayFast).toInt();
	Limit((int)modeFlash,mode,(int)modeAccumulate);

	zoom  = settings.value(key_memoryview_access_pixelzoom, zoom2).toInt();
	Limit((int)zoom2,zoom,(int)zoom4);
	pixel_size = zoom +(3-zoom2);

	rom_pixels.grow(machine->rom.Count());
	ram_pixels.grow(machine->ram.Count());

	CoreByte* data = machine->ram.Data();
	for(uint i = machine->ram.Count();i--;) data[i] |= cpu_r_access | cpu_w_access | cpu_x_access;
	data = machine->rom.Data();
	for(uint i = machine->rom.Count();i--;) data[i] |= cpu_r_access | cpu_w_access | cpu_x_access;
	machine->cpu_options |= cpu_r_access | cpu_w_access | cpu_x_access;

// validate bytes_per_row and rows:
	uint log2_bytes_per_row = min(4,LOG2(bytes_per_row/32)); bytes_per_row = 32<<log2_bytes_per_row;
	Limit(2,rows,data_size/bytes_per_row);

// create Widgets:
// note: sie erscheinen in der Kopfzeile in der Reihenfolge, in der sie zum head_widget hinzugefügt werden.

	combobox_decaymode = new QComboBox(head_widget);
	combobox_decaymode->setFixedSize(75,COMBOBOX_HEIGHT);
	combobox_decaymode->addItems(QStringList()<<"Flash"<<"Decay fast"<<"Decay slow"<<"Accumulate");
	combobox_decaymode->setCurrentIndex(mode);
	combobox_decaymode->setFocusPolicy(Qt::NoFocus);
	connect(combobox_decaymode,SIGNAL(currentIndexChanged(int)),this,SLOT(setDecayMode(int)));

	combobox_pixelzoom = new QComboBox(head_widget);
	combobox_pixelzoom->setFixedSize(65,COMBOBOX_HEIGHT);
	combobox_pixelzoom->addItems(QStringList()<<"2x2"<<"3x3"<<"4x4");
	combobox_pixelzoom->setCurrentIndex(zoom);
	combobox_pixelzoom->setFocusPolicy(Qt::NoFocus);
	connect(combobox_pixelzoom,SIGNAL(currentIndexChanged(int)),this,SLOT(setPixelZoom(int)));

	combobox_bytes_per_row = new QComboBox(head_widget);
	combobox_bytes_per_row->setFixedSize(60,COMBOBOX_HEIGHT);
	combobox_bytes_per_row->addItems(QStringList()<<"32"<<"64"<<"128"<<"256"<<"512");
	combobox_bytes_per_row->setCurrentIndex(log2_bytes_per_row);
	combobox_bytes_per_row->setFocusPolicy(Qt::NoFocus);
	connect(combobox_bytes_per_row,SIGNAL(currentIndexChanged(int)),this,SLOT(setBytesPerRow(int)));

	graphics_view	 = new GWidgetRGB(data_widget);

// start update:
	first_update = true;
}


MemoryAccessInspector::~MemoryAccessInspector()
{
	settings.setValue(key_memoryview_access_pixelzoom, zoom);
	settings.setValue(key_memoryview_access_decaymode, mode);
}


//helper
void MemoryAccessInspector::set_self_to_required_size(bool send_signal)
{
	int fix_w = bytes_per_row*pixel_size + scrollbar_width + (L_MARGIN+R_MARGIN);
	int max_h = data_size / bytes_per_row * pixel_size;
	int min_h = min(max_h,16*pixel_size);

	max_h += data_widget->y() + V_MARGINS;
	min_h += data_widget->y() + V_MARGINS;

	if( fix_w!=minimumWidth() || fix_w!=maximumWidth() || max_h!=maximumHeight() || min_h!=minimumHeight() || pixel_size!=sizeIncrement().height() )
	{
		setFixedWidth(fix_w);
		setMinimumHeight(min_h);
		setMaximumHeight(max_h);
		setSizeIncrement(pixel_size,pixel_size);

		if(send_signal) emit sizeConstraintsChanged();
		updateScrollbar();
	}
}

// virtual: Qt
void MemoryAccessInspector::resizeEvent(QResizeEvent* e)
{
	MemoryInspector::resizeEvent(e);

	int w = pixel_size*bytes_per_row;
	int h = data_widget->height(); // pixel_size*rows;
	rows = h/pixel_size;

	graphics_view->setGeometry(0,0,w,h);

	updateScrollbar();
//	updateDisplayedData();
}


//virtual slot: callback from MachineController:
void MemoryAccessInspector::memoryConfigChanged()
{
	if(!machine || !object) return;

	MemoryInspector::memoryConfigChanged();

	rom_pixels.resize(machine->rom.Count());
	ram_pixels.resize(machine->ram.Count());
	if(graphics_view->canvas) set_self_to_required_size(yes/*send signal*/);
}


//slot for combobox_decaymode:
void MemoryAccessInspector::setDecayMode(int m)
{
	if(m!=mode)
	{
		if(m==modeDecayFast)
		{
			prepare_for_fast_decay(rom_pixels.Data(),rom_pixels.count());
			prepare_for_fast_decay(ram_pixels.Data(),ram_pixels.count());
		}
		mode = m;
	}
}


//slot for combobox_pixelzoom:
void MemoryAccessInspector::setPixelZoom(int z)
{
	if(z==zoom) return;

	zoom = z;
	pixel_size = z-zoom2+3;
	set_self_to_required_size(yes);
}


////virtual slot for scrollbar:
//void MemoryAccessInspector::setScrollPosition(int row)
//{
//	setBaseAddress(row*bytes_per_row);
//}

//slot for combobox_bytes_per_row:
// may be called with new_bytes_per_row or index in combobox_width
void MemoryAccessInspector::setBytesPerRow(int w)
{
	if(w<16) w = 32<<w;
	if(w==bytes_per_row) return;

	bytes_per_row = w;
	display_base_address = display_base_address / w * w;

	set_self_to_required_size(yes);
}


// virtual slot for combobox_datasource:
void MemoryAccessInspector::setDataSource(int newdatasource)
{
	if(newdatasource==data_source) return;

	uint32 old_size = data_size;
	MemoryInspector::setDataSource(newdatasource);
	uint32 new_size = data_size;

	if(old_size!=new_size) set_self_to_required_size(yes);
}


// virtual slot for combobox_memorypage:
void MemoryAccessInspector::setMemoryPage(int newpage)
{
	if(newpage<0) return;	// empty comboBox
	if(newpage==(data_source==RamPages?ram_page:rom_page)) return;

	uint32 old_size = data_size;
	MemoryInspector::setMemoryPage(newpage);
	uint32 new_size = data_size;

	if(old_size!=new_size) set_self_to_required_size(yes);
}



// color definition for execute, read and write:
// bit 2,1,0 = x,w,r
const uint32 x = 0x00ff0000, r = 0x000000ff, w = 0x0000ff00;
static uint32 rgb[] = {0,r,w,r+w,x,x+r,x+w,x+r+w};


// helper for updateDisplayedData():
// copy access bits from memory to temp buffer
// and clear access bits in memory
void MemoryAccessInspector::copy_access_bits_to_pixels(Mem& mem, uint32* pixels)
{
	CoreByte* memptr = mem.Data();
	int cnt = mem.Count();
	int bits;

	for(int i=0;i<cnt;i++)
	{
		if(( bits = ((~memptr[i])/cpu_r_access)&7 ))		// bits 0,1,2 = r,w,x
		{
			pixels[i] |= rgb[bits];
			memptr[i] |= cpu_r_access | cpu_w_access | cpu_x_access;
		}
	}
}


// helper for setDecayMode():
void MemoryAccessInspector::prepare_for_fast_decay(uint32* pixels, int cnt)
{
	uint32 pixel;

	for(int i=0;i<cnt;i++)
	{
		if(( pixel = pixels[i] ))
		{
			uint32 r = ((pixel&0xff0000)>>16)/5*5;
			uint32 g = ((pixel&0x00ff00)>>8) /5*5;
			uint32 b = ((pixel&0x0000ff))    /5*5;
			pixels[i] = (r<<16)+(g<<8)+b;
		}
	}

}


// helper for updateDisplayedData():
void MemoryAccessInspector::decay_pixel(uint32* pixels, int cnt, int decay)
{
	uint32 pixel;
	uint32 r = decay<<16;
	uint32 g = decay<<8;
	uint32 b = decay;

	for(int i=0;i<cnt;i++)
	{
		if(( pixel = pixels[i] ))
		{
			if(pixel&0x0000ff) pixel -= b;
			if(pixel&0x00ff00) pixel -= g;
			if(pixel&0xff0000) pixel -= r;
			pixels[i] = pixel;
		}
	}
}


// helper for updateDisplayedData:
inline uint32 weighted_color(uint32 pixel)
{
	if(pixel&0x00ffff00)
	{
		uint32 r = uint8(pixel>>16);
		uint32 g = uint8(pixel>>8);
		uint32 b = uint8(pixel);
		return ((r-r/4)<<16) + ((g-g/3)<<8) + b;
	}
	else return pixel;
}


// timer: refresh displayed data
void MemoryAccessInspector::updateDisplayedData()
{
// Feststellen, ob das Toolwindow sichtbar ist:
// das ist gar nicht so einfach...
	if(!appl->isActiveApplication()) return;				 // sonst raus
	if(machine->controller!=front_machine_controller)return; // sonst raus

// Beim ersten Mal erst noch das Widget resizen:
	if(first_update)
	{
		first_update=false;
		set_self_to_required_size(yes);
	}

// update parent:
	MemoryInspector::updateDisplayedData();
	if(graphics_view->canvas==NULL) return;

// fade old pixels according to 'mode' setting:
	switch(mode)
	{
	case modeFlash:	// show only what was accessed since last update
					// TODO: needs only clear visible bytes
		memset(rom_pixels.Data(),0,rom_pixels.count()*sizeof(uint32));
		memset(ram_pixels.Data(),0,ram_pixels.count()*sizeof(uint32));
		break;
	case modeDecayFast:	// decay previous access
		decay_pixel(rom_pixels.Data(),rom_pixels.count(),5);
		decay_pixel(ram_pixels.Data(),ram_pixels.count(),5);
		break;
	case modeDecaySlow:	// decay previous access
		decay_pixel(rom_pixels.Data(),rom_pixels.count(),1);
		decay_pixel(ram_pixels.Data(),ram_pixels.count(),1);
		break;
	case modeAccumulate:// don't decay/clear old access
		break;
	}

// update with new access bits:
	XXXASSERT(machine->rom.Count()==rom_pixels.count());
	XXXASSERT(machine->ram.Count()==ram_pixels.count());
	copy_access_bits_to_pixels(machine->rom,rom_pixels.Data());
	copy_access_bits_to_pixels(machine->ram,ram_pixels.Data());

// validate display_base_address:
	if( (uint32)display_base_address > (uint32)(data_size-rows*bytes_per_row) )
	{
		Limit(0,display_base_address,data_size-rows*bytes_per_row);
	}

// create map for row->pixels
	Array<QRgb*> pixelrows;
	pixelrows.grow(rows);
	if(XXXSAFE){ for(uint i=0;i<pixelrows.count();i++) { ASSERT(pixelrows[i]==0); } }

	switch(data_source)
	{
	case AsSeenByCpu:
		{
			uint32* romptr = machine->rom.Data();	uint romsize = machine->rom.Count();
			uint32* ramptr = machine->ram.Data();	uint ramsize = machine->ram.Count();
			Z80* cpu = machine->cpu;
			for( int r = min(rows,(0x10000-display_base_address)/bytes_per_row); r-- ; )
			{
				CoreByte* p = cpu->rdPtr(display_base_address+r*bytes_per_row);
				if(p>=romptr&&p<romptr+romsize) pixelrows[r] = rom_pixels.Data() + (p-romptr); else
				if(p>=ramptr&&p<ramptr+ramsize) pixelrows[r] = ram_pixels.Data() + (p-ramptr); // else unmapped cpu address -> NULL
			}
			break;
		}
	case AllRam:
	case RamPages:
		{
			uint32* p = &ram_pixels[data_start+display_base_address];
			int r = min( rows, int(data_size-display_base_address)/bytes_per_row );
			while(r--) { pixelrows[r] = p+r*bytes_per_row; }
			break;
		}
	case AllRom:
	case RomPages:
		{
			uint32* p = &rom_pixels[data_start+display_base_address];
			int r = min( rows, int(data_size-display_base_address)/bytes_per_row );
			while(r--) { pixelrows[r] = p+r*bytes_per_row; }
			break;
		}
	}

// display according to 'space' and 'zoom':
	graphics_view->canvas->fill(Qt::black);
	switch(zoom)
	{
	case zoom2:		// 2*2 pixel
		{
			uint32 pixel;
			const int d = 3;
			XXXASSERT(graphics_view->canvas->width()>=d*bytes_per_row);
			XXXASSERT(graphics_view->canvas->height()>=d*rows);

			for(int r=0;r<rows;r++)
			{
				QRgb* q = pixelrows[r]; if(q==NULL)
				{
					continue;
				}
				QRgb* z0 = (QRgb*)graphics_view->canvas->scanLine(r*d);
				QRgb* z1 = (QRgb*)graphics_view->canvas->scanLine(r*d+1);

				for(int zi=0; zi<d*bytes_per_row; zi+=d)
				{
					if(( pixel = *q++ ))
					{
						z0[zi]   = z1[zi]   =
						z0[zi+1] = z1[zi+1] = weighted_color(pixel);
					}
				}
			}
			break;
		}

	case zoom3:		// 3*3 pixel
		{
			uint32 pixel;
			const int d = 4;
			for( int r=0; r<rows; r++ )
			{
				uint32* q  = pixelrows[r]; if(q==NULL) continue;
				uint32* z0 = (uint32*)graphics_view->canvas->scanLine(r*d);
				uint32* z1 = (uint32*)graphics_view->canvas->scanLine(r*d+1);
				uint32* z2 = (uint32*)graphics_view->canvas->scanLine(r*d+2);

				for(int zi=0;zi<d*bytes_per_row;zi+=d)
				{
					if(( pixel = *q++ ))
					{
						z0[zi]   = z1[zi]   = z2[zi]   =
						z0[zi+1] = z1[zi+1] = z2[zi+1] =
						z0[zi+2] = z1[zi+2] = z2[zi+2] = weighted_color(pixel);
					}
				}
			}
			break;
		}

	case zoom4:		// 4*4 pixel + delimiter
		{
			uint32 pixel;
			const int d = 5;
			for( int r=0; r<rows; r++ )
			{
				uint32* q  = pixelrows[r]; if(q==NULL) continue;
				uint32* z0 = (uint32*)graphics_view->canvas->scanLine(r*d);
				uint32* z1 = (uint32*)graphics_view->canvas->scanLine(r*d+1);
				uint32* z2 = (uint32*)graphics_view->canvas->scanLine(r*d+2);
				uint32* z3 = (uint32*)graphics_view->canvas->scanLine(r*d+3);

				for(int zi=0;zi<d*bytes_per_row;zi+=d)
				{
					if(( pixel = *q++ ))
					{
						z0[zi]   = z1[zi]   = z2[zi]   = z3[zi]   =
						z0[zi+1] = z1[zi+1] = z2[zi+1] = z3[zi+1] =
						z0[zi+2] = z1[zi+2] = z2[zi+2] = z3[zi+2] =
						z0[zi+3] = z1[zi+3] = z2[zi+3] = z3[zi+3] = weighted_color(pixel);
					}
				}
			}
			break;
		}
	}

	graphics_view->update();
	update_tooltip();
}


/*	Tooltip aktualisieren:
	aber nur, wenn die Maus über dem MemoryView hovert
	da QToolTip::showText(…) die App zwangsweise in den Vordergrund (zurück-) bringt.
*/
void MemoryAccessInspector::update_tooltip()
{
	XXLogIn("updateTooltip");

	QPoint gpos = QCursor::pos();
	if(QApplication::topLevelAt(gpos)!=window()) { graphics_view->x=-99; return; }
	QPoint pos  = graphics_view->mapFromGlobal(gpos);

	uint x = graphics_view->x = pos.x()+mouse_x_offset;	if( x >= (uint)graphics_view->width() ) return;
	uint y = graphics_view->y = pos.y()+mouse_y_offset;	if( y >= (uint)graphics_view->height() ) return;
	graphics_view->w = pixel_size;

	uint32 addr = display_base_address + x/pixel_size + y/pixel_size*bytes_per_row;
	if(addr>=(uint32)data_size) { graphics_view->x=-99; return; }

	uint byte = data_source==AsSeenByCpu ? machine->cpu->peek(addr) :
				data_source==RomPages || data_source==AllRom ? (uint8)machine->rom[data_start+addr] :
				(uint8)machine->ram[data_start+addr];

	QToolTip::showText(gpos, usingstr("$%04X: $%02X",(uint)addr,byte), graphics_view, QRect());
}





//void MemoryAccessInspector::showEvent(QShowEvent*)
//{
//	XLogIn("MemoryAccessInspector::showEvent");
//	visible = yes;
//}


//void MemoryAccessInspector::hideEvent(QHideEvent*)
//{
//	XLogIn("MemoryAccessInspector::hideEvent");
//	visible = no;
//}


//bool MemoryAccessInspector::event(QEvent* e)
//{
//	XLogIn("MemoryAccessInspector:event: %s",QEventTypeStr(e->type()));

//	if(e->type()==QEvent::WindowActivate) visible=yes;
//	if(e->type()==QEvent::WindowDeactivate) visible=no;

//	return MemoryInspector::event(e);
//}























