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

	This file is free software.

	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 appears in all copies and
	that both that copyright notice, this permission notice and the
	following disclaimer appear in supporting documentation.

	THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT ANY WARRANTY,
	NOT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR
	A PARTICULAR PURPOSE, AND IN NO EVENT SHALL THE COPYRIGHT HOLDER
	BE LIABLE FOR ANY DAMAGES ARISING FROM THE USE OF THIS SOFTWARE,
	TO THE EXTENT PERMITTED BY APPLICABLE LAW.
*/

#define LOGLEVEL 1
#include "kio/kio.h"
#include "GTermWindow.h"
#include <QPainter>
#include <QKeyEvent>
#include <cmath>
#include <QOpenGLWidget>
#include <QOpenGLWindow>
#include <QPicture>
#include <QScreen>
#include <QApplication>

using SUPER = QOpenGLWindow;
namespace gterm
{

static constexpr int STATS = 1;

void statistics(int id)
{
	// id=0: paintEvent start
	// id=1: paintEvent end
	// id=2: frameSwapped

	static double time0 = now(), time1 = time0;

	static double min0 = 0.1, max0=0.0, avg0=0.017;
	static double min1 = 0.1, max1=0.0, avg1=0.000;

	switch(id)
	{
	case 0: // paintEvent start
	{
		double d = now()-time0;
		time0 += d;

		min0 = min(min0,d);
		max0 = max(max0,d);
		avg0 = avg0*0.99 + d*0.01;
		break;
	}
	case 1: // paintEvent end
	{
		double d = now()-time0;
		time1 = time0 + d;

		min1 = min(min1,d);
		max1 = max(max1,d);
		avg1 = avg1*0.99 + d*0.01;

		static uint8 n = 0;
		if(++n==0)
		{
			qDebug( "frames =%6.2f|%6.2f|%6.2f  paint =%5.2f|%5.2f|%5.2f",
					min0*1000, avg0*1000, max0*1000,	  // duration between paint events [ms]
					min1*1000, avg1*1000, max1*1000);	  // time for drawing [ms]
			std::swap(min0,max0);
			std::swap(min1,max1);
		}
		break;
	}
	}
}


GTermWindow::GTermWindow (QSize size, QRgb bkg, ImageType image_type, ScalingMode scaling_mode, DisplayStyle display_style)
	: SUPER(NoPartialUpdate,nullptr)
	, scaling_mode(scaling_mode)
	, display_style(display_style)
{
	switch (image_type)
	{
	case STATIC_IMAGE:
		image = newStaticImage(size,bkg);
		break;
	case DYNAMIC_IMAGE:
		image = newDynamicImage(size,bkg);
		break;
	}

	if (display_style & DS_FRAMELESS)
	{
		setFlags(flags() | Qt::WindowType::FramelessWindowHint);
	}

	if (display_style & DS_TRANSPARENT)
	{
		QSurfaceFormat format;
		format.setAlphaBufferSize(8);
		this->setFormat(format);
	}

	switch (scaling_mode)
	{
	case SM_FIXED:
		setMinimumSize(size);
		setMaximumSize(size);
		break;
	case SM_IGNORE:
		break;
	case SM_PANNING:
		setMaximumSize(size);
		break;
	case SM_SCALING:
		break;
	}

	resize(size);
	reset();
}

void GTermWindow::reset()
{
	xlogline(__func__);

	enable_keyboard_modifier_events = false;
	enable_key_auto_repeat_events = true;
	enable_all_mouse_move_events = false;

	nvptr(image)->erase();
}

void GTermWindow::quit()
{
	xlogline(__func__);

	QApplication::quit();
}

void GTermWindow::paintEvent (QPaintEvent*)
{
	if (STATS) statistics(0);

	QPainter p(this);
	p.setCompositionMode(QPainter::CompositionMode_Source);
	QColor bg = QColor::fromRgba(image->background_color);

	// NOTE: the first QPainter method which actually draws to the window blocks until vsync(?) ~ 10ms

	if (is_static())
	{
		NVPtr<StaticImage> image = getStaticImage();

		switch(scaling_mode)
		{
		case SM_FIXED:
			assert(topleft == QPoint(0,0));
			assert(image->size() == size());
			break;
		case SM_IGNORE:
			{// fill padding area around image, if any:
				int x2,y2;
				if (topleft.x() > 0) p.fillRect(0,0, topleft.x(),height(), bg);
				if (topleft.y() > 0) p.fillRect(0,0, width(),topleft.y(),  bg);
				if ((x2 = topleft.x() + image->width())  < width())  p.fillRect(x2,0, width()-x2,height(), bg);
				if ((y2 = topleft.y() + image->height()) < height()) p.fillRect(0,y2, width(),height()-y2, bg);
			}
			break;
		case SM_PANNING:
			assert(topleft.x() <= 0 && topleft.y() <= 0);
			assert(width() <= topleft.x()+image->width() && height() <= topleft.x()+image->height());
			break;
		case SM_SCALING:
			assert(topleft==QPoint(0,0));
			qreal sx = qreal(width()) / image->width();
			qreal sy = qreal(height()) / image->height();
			p.scale(sx,sy);
			break;
		}

		p.drawImage(topleft, image->image);
	}
	else
	{
		NVPtr<DynamicImage> image = getDynamicImage();

		p.fillRect(0,0,width(),height(), bg);

		switch(scaling_mode)
		{
		case SM_FIXED:	// window can't be resized => no transformation needed
			assert(topleft == QPoint(0,0));
			break;
		case SM_IGNORE:	// ignore scaling => no transformation needed
			p.translate(topleft);
			break;
		case SM_PANNING:
			p.translate(topleft);
			break;
		case SM_SCALING: // scale to window size => scale
			assert(topleft == QPoint(0,0));
			qreal sx = qreal(width()) / image->size().width();
			qreal sy = qreal(height()) / image->size().height();
			p.scale(sx,sy);
			break;
		}

		p.setCompositionMode(QPainter::CompositionMode_SourceOver); // default
		image->run(p,false);
	}

	if (STATS) statistics(1);

	update(); // trigger next paintEvent()

	if (frame_swapped_event) frame_swapped_event();
}

void GTermWindow::resizeEvent (QResizeEvent* e)
{
	xlogline("%s(%i,%i)",__func__, e->size().width(),e->size().height());

	// also called for enter/leave fullscreen mode

	SUPER::resizeEvent(e);

	switch(scaling_mode)
	{
	case SM_FIXED:
		assert(topleft == QPoint(0,0));
		image->size() = e->size();
		break;
	case SM_IGNORE:
		break;
	case SM_PANNING:
	{
		int newwidth  = e->size().width();
		int newheight = e->size().height();
		assert(topleft.x() <= 0 && topleft.y() <= 0);
		assert(newwidth <= image->width() && newheight <= image->height());

		QPoint newtopleft = topleft;
		if (topleft.x() < newwidth - image->width())
			newtopleft.setX(newwidth-image->width());
		if (topleft.y() < newheight - image->height())
			newtopleft.setY(newheight-image->height());

		// TODO: update scrollbars
		break;
	}
	case SM_SCALING:
		assert(topleft == QPoint(0,0));
		break;
	}

	if (resize_event) resize_event(e->size());
}

void GTermWindow::showEvent (QShowEvent* e)
{
	xlogline("%s",__func__);				// showEvent
	//xlogline("%s",__FUNCTION__);			// showEvent
	//xlogline("%s",__PRETTY_FUNCTION__);	// virtual void gterm::GTermWindow::showEvent(QShowEvent *)

	switch(scaling_mode)
	{
	case SM_FIXED:
		assert(topleft == QPoint(0,0));
		assert(image->size() == size());
		break;
	case SM_IGNORE:
		break;
	case SM_PANNING:
		assert(topleft.x() <= 0 && topleft.y() <= 0);
		assert(width() <= topleft.x()+image->width() && height() <= topleft.x()+image->height());
		break;
	case SM_SCALING:
		assert(topleft == QPoint(0,0));
		break;
	}

	SUPER::showEvent(e);
	//QPainter(this).fillRect(0,0,size.width(),size.height(),colors.background);	// doesn't work
	if (show_event) show_event(true);
}

void GTermWindow::hideEvent (QHideEvent* e)
{
	xlogline("%s",__func__);

	SUPER::hideEvent(e);
	//QPainter(this).fillRect(0,0,size.width(),size.height(),colors.background);	// doesn't work
	if (show_event) show_event(false);
}

void GTermWindow::focusInEvent (QFocusEvent* e)
{
	xlogline("%s",__func__);

	SUPER::focusInEvent(e);
	if (focus_event) focus_event(true);
}

void GTermWindow::focusOutEvent (QFocusEvent* e)
{
	xlogline("%s",__func__);

	SUPER::focusOutEvent(e);
	if (focus_event) focus_event(false);
}

static constexpr KeyModifiers mods_for_mods(Qt::KeyboardModifiers qmods)
{
	// optimized by compiler:
	KeyModifiers mods = KM_NONE;
	if (qmods & Qt::ShiftModifier) mods |= KM_SHIFT;
	if (qmods & Qt::ControlModifier) mods |= KM_CONTROL;
	if (qmods & Qt::AltModifier) mods |= KM_ALT;
	if (qmods & Qt::MetaModifier) mods |= KM_META;
	if (qmods & Qt::KeypadModifier) mods |= KM_KEYPAD;
	if (qmods & Qt::GroupSwitchModifier) mods |= KM_ALTGR;
	return mods;
}

static constexpr KeyModifiers mods_for_key(Qt::Key key)
{
	switch (key)
	{
	case Qt::Key_Shift:  return KM_SHIFT;
	case Qt::Key_Control:return KM_CONTROL;
	case Qt::Key_Alt:	 return KM_ALT;
	case Qt::Key_Meta:	 return KM_META;
	case Qt::Key_AltGr:	 return KM_ALTGR;
	default:			 return KM_NONE;
	}
}

void GTermWindow::keyPressEvent (QKeyEvent* e)
{
	// QKeyEvent contains:
	// text()			--> resulting printable char or text or empty
	// key()			--> Qt::Key, if possible same as text() but "as printed on the key cap" and all special keys
	// modifiers()		--> BUG: modifiers() are wrong after modifier key up/down!
	// nativeScanCode() --> USB keyboard keys are mostly standardized

	xlogline("keyPressEvent   \"%s\" key=0x%08x scancode: %u modifiers: 0x%08x",
			e->text().count()&&e->text().at(0)>=127 ? catstr("0x",hexstr(e->text().at(0).unicode(),4)) :
			e->text().count()&&e->text().at(0)<=32  ? catstr("0x",hexstr(e->text().at(0).unicode(),2)) :
			e->text().toUtf8().data(),
			e->key(), e->nativeScanCode(), uint(e->modifiers()));

	// close window and quit application if quitOnLastWindowClosed is set:
	if (e->modifiers() == Qt::ControlModifier && e->key() == 'Q')
		close();

	if (key_down_event)
	{
		if (e->isAutoRepeat() && !enable_key_auto_repeat_events) return;

		KeyModifiers xmod = mods_for_key(Qt::Key(e->key()));
		if (xmod && !enable_keyboard_modifier_events) return;

		KeyModifiers modifiers = mods_for_mods(e->modifiers()) | xmod;

		uint scancode = e->nativeScanCode();

		// unicode is either ucs2 or a Qt::Key code.
		// Qt::Key codes are 0x0100xxxx or on rare devices 0x01xxxxxx.
		// unicode and modifiers can be ORed

		assert(e->text().count() <= 1);
		uint unicode = e->text().count() ? e->text().at(0).unicode() :
			 (uint(e->key()) & 0xff000000) == 0x01000000 ? uint(e->key()) : 0;

		key_down_event(unicode,scancode,modifiers);
	}
}

void GTermWindow::keyReleaseEvent (QKeyEvent* e)
{
	xlogline("keyReleaseEvent \"%s\" key=0x%08x scancode: %u modifiers: 0x%08x",
			e->text().count()&&e->text().at(0)>=127 ? catstr("0x",hexstr(e->text().at(0).unicode(),4)) :
			e->text().count()&&e->text().at(0)<=32  ? catstr("0x",hexstr(e->text().at(0).unicode(),2)) :
			e->text().toUtf8().data(),
			e->key(), e->nativeScanCode(), uint(e->modifiers()));

	if (key_up_event)
	{
		KeyModifiers xmod = mods_for_key(Qt::Key(e->key()));
		if (xmod && !enable_keyboard_modifier_events) return;

		KeyModifiers modifiers = mods_for_mods(e->modifiers()) & ~xmod;

		uint scancode = e->nativeScanCode();

		assert(e->text().count() <= 1);
		uint unicode = e->text().count() ? e->text().at(0).unicode() :
			 (uint(e->key()) & 0xff000000) == 0x01000000 ? uint(e->key()) : 0;

		key_up_event(unicode,scancode,modifiers);
	}
}

void GTermWindow::mousePressEvent(QMouseEvent* e)
{
	xlogline("%s",__func__);

	if (mouse_button_down_event)
		mouse_button_down_event(e->pos(), e->button(), e->buttons(), e->modifiers(), e->timestamp());
}

void GTermWindow::mouseReleaseEvent(QMouseEvent* e)
{
	xlogline("%s",__func__);

	if (mouse_button_up_event)
		mouse_button_up_event(e->pos(), e->button(), e->buttons(), e->modifiers(), e->timestamp());
}

void GTermWindow::mouseMoveEvent(QMouseEvent* e)
{
	xxlogline("%s",__func__);

	if (mouse_move_event && (e->buttons() || enable_all_mouse_move_events))
		mouse_move_event(e->pos(), e->buttons(), e->modifiers());
}

void GTermWindow::setScalingMode(ScalingMode sm)
{
	xlogline("%s",__func__);

	if (sm == scaling_mode) return;
	else scaling_mode = sm;

	switch (sm)
	{
	case SM_FIXED:
	{
		topleft = QPoint(0,0);
		QSize sz = image->size();
		setMinimumSize(sz);
		setMaximumSize(sz);
		resize(sz);
		//TODO: remove scrollbars if it was panning
		break;
	}
	case SM_IGNORE:
	{
		setMinimumSize(QSize(32,32));
		setMaximumSize(screen()->size());
		//TODO: remove scrollbars if it was panning
		break;
	}
	case SM_PANNING:
	{
		topleft = QPoint(0,0);
		setMinimumSize(QSize(32,32));
		setMaximumSize(image->size());
		QSize sz = size();
		if (sz.width() > image->width()) sz.setWidth(image->width());
		if (sz.height() > image->height()) sz.setHeight(image->height());
		resize(sz);
		break;
	}
	case SM_SCALING:
	{
		topleft = QPoint(0,0);
		setMinimumSize(QSize(32,32));
		setMaximumSize(screen()->size());
		//TODO: remove scrollbars if it was panning
		break;
	}
	}
}

void GTermWindow::setDisplayStyle(DisplayStyle ds)
{
	xlogline("%s",__func__);

	uint toggled = display_style ^ ds;
	if (!toggled) return;
	display_style = ds;

	if (toggled & DS_FRAMELESS)	// toggle framed <-> frameless: OK
	{
		if (display_style & DS_FRAMELESS)
			setFlags(flags() | Qt::WindowType::FramelessWindowHint);
		else
			setFlags(flags() & ~Qt::WindowType::FramelessWindowHint);
	}

	if (toggled & DS_TRANSPARENT) // TOGGLE transparency: DOESN'T WORK
	{
		QSurfaceFormat format;
		int depth = ds & DS_TRANSPARENT ? 8 : 0;
		format.setAlphaBufferSize(depth);
		this->setFormat(format);
		assert(this->format().hasAlpha() == !!depth);
		assert(this->format().alphaBufferSize() == depth);
		hide();
		show();
	}
}

void GTermWindow::setBackgroundColor(QRgb color)
{
	xlogline("%s",__func__);

	nv(image)->background_color = color;
}

void GTermWindow::resizeWindow(QSize sz)
{
	xlogline("%s",__func__);

	// except for SM_FIXED resizing the window does not affect the image size

	if (sz == size()) return;

	switch(scaling_mode)
	{
	case SM_FIXED:
		// note: image will be resized in resizeEvent()
		break;
	case SM_IGNORE:
		break;
	case SM_PANNING:
		// TODO: handle topleft and scrollbars
		break;
	case SM_SCALING:
		break;
	}

	resize(sz);
}

void GTermWindow::resizeImage(QSize sz)
{
	xlogline("%s",__func__);

	// except for SM_FIXED resizing the image does not affect the window size
	// image contents are not modified by this function.

	if (sz == image->size()) return;

	switch(scaling_mode)
	{
	case SM_FIXED:
		// note: image will be resized in resizeEvent()
		resize(sz);
		break;
	case SM_IGNORE:
		image->size() = sz;
		break;
	case SM_PANNING:
		image->size() = sz;
		// TODO: handle topleft and scrollbars
		break;
	case SM_SCALING:
		image->size() = sz;
		break;
	}
}

} // namespace




























