/*
	Copyright 2006-2023 The QElectroTech Team
	This file is part of QElectroTech.

	QElectroTech is free software: you can redistribute it and/or modify
	it under the terms of the GNU General Public License as published by
	the Free Software Foundation, either version 2 of the License, or
	(at your option) any later version.

	QElectroTech 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.  See the
	GNU General Public License for more details.

	You should have received a copy of the GNU General Public License
	along with QElectroTech.  If not, see <http://www.gnu.org/licenses/>.
*/
#include "elementview.h"

#include "../qetapp.h"
#include "UndoCommand/pastepartscommand.h"
#include "ui/qetelementeditor.h"
/**
	Constructeur
	@param scene ElementScene visualisee par cette ElementView
	@param parent QWidget parent de cette ElementView
*/
ElementView::ElementView(ElementScene *scene, QWidget *parent) :
	QGraphicsView(scene, parent),
	m_scene(scene),
	offset_paste_count_(0)
{
	grabGesture(Qt::PinchGesture);
	setViewportUpdateMode(QGraphicsView::BoundingRectViewportUpdate);
	setInteractive(true);
	setAlignment(Qt::AlignHCenter | Qt::AlignVCenter);
	setResizeAnchor(QGraphicsView::AnchorUnderMouse);
	setTransformationAnchor(QGraphicsView::AnchorUnderMouse);
	zoomReset();
	connect(m_scene, SIGNAL(pasteAreaDefined(const QRectF &)), this, SLOT(pasteAreaDefined(const QRectF &)));
	connect(m_scene, SIGNAL(needZoomFit()), this, SLOT(zoomFit()));
}

/// Destructeur
ElementView::~ElementView()
{
}

/// @return l'ElementScene visualisee par cette ElementView
ElementScene *ElementView::scene() const
{
	return(m_scene);
}

/**
	@return le rectangle de l'element visualise par cet ElementView
*/
QRectF ElementView::viewedSceneRect() const
{
	// recupere la taille du widget viewport
	QSize viewport_size = viewport() -> size();

	// recupere la transformation viewport -> scene
	QTransform view_to_scene   = viewportTransform().inverted();

	// mappe le coin superieur gauche et le coin inferieur droit de la viewport sur la scene
	QPointF scene_left_top     = view_to_scene.map(QPointF(0.0, 0.0));
	QPointF scene_right_bottom = view_to_scene.map(QPointF(viewport_size.width(), viewport_size.height()));

	// en deduit le rectangle visualise par la scene
	return(QRectF(scene_left_top, scene_right_bottom));
}

/**
	Definit l'ElementScene visualisee par cette ElementView
	@param s l'ElementScene visualisee par cette ElementView
*/
void ElementView::setScene(ElementScene *s) {
	QGraphicsView::setScene(s);
	m_scene = s;
}

/**
	Set the Diagram in visualisation mode
*/
void ElementView::setVisualisationMode()
{
	setDragMode(ScrollHandDrag);
	setInteractive(false);
	emit(modeChanged());
}

/**
	Set the Diagram in Selection mode
*/
void ElementView::setSelectionMode()
{
	setDragMode(RubberBandDrag);
	setInteractive(true);
	emit(modeChanged());
}

/**
	Agrandit le schema (+33% = inverse des -25 % de zoomMoins())
*/
void ElementView::zoomIn()
{
	adjustSceneRect();
	scale(4.0/3.0, 4.0/3.0);
}

/**
	Retrecit le schema (-25% = inverse des +33 % de zoomPlus())
*/
void ElementView::zoomOut()
{
	adjustSceneRect();
	scale(0.75, 0.75);
}

/**
	Agrandit le schema avec le trackpad
*/
void ElementView::zoomInSlowly()
{
	scale(1.02, 1.02);
}

/**
	Retrecit le schema avec le trackpad
*/
void ElementView::zoomOutSlowly()
{
	scale(0.98, 0.98);
}

/**
	Agrandit ou rectrecit le schema de facon a ce que tous les elements du
	schema soient visibles a l'ecran. S'il n'y a aucun element sur le schema,
	le zoom est reinitialise
*/
void ElementView::zoomFit()
{
	resetSceneRect();
	fitInView(sceneRect(), Qt::KeepAspectRatio);
}

/**
	Reinitialise le zoom
*/
void ElementView::zoomReset()
{
	resetSceneRect();
	resetTransform();
	scale(4.0, 4.0);
}

/**
	@brief ElementView::adjustSceneRect
	Adjust the scenRect, so that he include all primitives of element
	plus the viewport of the scene with a margin of 1/3 of herself
*/
void ElementView::adjustSceneRect()
{
	QRectF esgr = m_scene -> elementSceneGeometricRect();
	QRectF vpbr = mapToScene(this -> viewport()->rect()).boundingRect();
	QRectF new_scene_rect = vpbr.adjusted(-vpbr.width()/3, -vpbr.height()/3, vpbr.width()/3, vpbr.height()/3);
	setSceneRect(new_scene_rect.united(esgr));
}

/**
	@brief ElementView::resetSceneRect
	reset le sceneRect (zone du schéma visualisée par l'ElementView) afin que
	celui-ci inclut uniquement les primitives de l'élément dessiné.
*/
void ElementView::resetSceneRect()
{
	setSceneRect(m_scene -> elementSceneGeometricRect());
}

/**
	Gere le fait de couper la selection = l'exporter en XML dans le
	presse-papier puis la supprimer.
*/
void ElementView::cut()
{
	// delegue cette action a la scene
	m_scene -> cut();
	offset_paste_count_ = -1;
}

/**
	Gere le fait de copier la selection = l'exporter en XML dans le
	presse-papier.
*/
void ElementView::copy()
{
	// delegue cette action a la scene
	m_scene -> copy();
	offset_paste_count_ = 0;
}

/**
	Gere le fait de coller le contenu du presse-papier = l'importer dans
	l'element. Cette methode examine le contenu du presse-papier. Si celui-ci
	semble avoir ete copie depuis cet element, il est colle a cote de sa zone
	d'origine ; s'il est recolle, il sera colle un cran a cote de la zone deja
	recollee, etc.
	Sinon, cette methode demande a l'utilisateur de definir la zone ou le
	collage devra s'effectuer.
	@see pasteAreaDefined(const QRectF &)
*/
void ElementView::paste()
{
	QString clipboard_text = QApplication::clipboard() -> text();
	if (clipboard_text.isEmpty()) return;

	QDomDocument document_xml;
	if (!document_xml.setContent(clipboard_text)) return;

	if (m_scene -> wasCopiedFromThisElement(clipboard_text)) {
		// copier/coller avec decalage
		pasteWithOffset(document_xml);
	} else {
		// copier/coller par choix de la zone de collage
		QRectF pasted_content_bounding_rect = m_scene -> boundingRectFromXml(document_xml);
		if (pasted_content_bounding_rect.isEmpty()) return;

		to_paste_in_area_ = clipboard_text;
		getPasteArea(pasted_content_bounding_rect);
	}
}

/**
	Colle le contenu du presse-papier en demandant systematiquement a
	l'utilisateur de choisir une zone de collage
*/
void ElementView::pasteInArea()
{
	QString clipboard_text = QApplication::clipboard() -> text();
	if (clipboard_text.isEmpty()) return;

	QDomDocument document_xml;
	if (!document_xml.setContent(clipboard_text)) return;

	QRectF pasted_content_bounding_rect = m_scene -> boundingRectFromXml(document_xml);
	if (pasted_content_bounding_rect.isEmpty()) return;

	// copier/coller par choix de la zone de collage
	to_paste_in_area_ = clipboard_text;
	getPasteArea(pasted_content_bounding_rect);
}

/**
	Gere le fait de coller le contenu du presse-papier = l'importer dans
	l'element. Cette methode examine le contenu du presse-papier. Si celui-ci
	est exploitable, elle le colle a la position passee en parametre.
	@see pasteAreaDefined(const QRectF &)
	@param position Point de collage
*/
ElementContent ElementView::paste(const QPointF &position) {
	QString clipboard_text = QApplication::clipboard() -> text();
	if (clipboard_text.isEmpty()) return(ElementContent());

	QDomDocument document_xml;
	if (!document_xml.setContent(clipboard_text)) return(ElementContent());

	// objet pour recuperer le contenu ajoute au schema par le coller
	return(paste(document_xml, position));
}

/**
	@param to_paste Rectangle englobant les parties a coller
*/
void ElementView::getPasteArea(const QRectF &to_paste) {
	// on copie le rectangle fourni - on s'interesse a ses dimensions, pas a sa position
	QRectF used_rect(to_paste);

	// on lui attribue pour centre l'origine du repere
	if (underMouse()) {
		used_rect.moveCenter(mapToScene(mapFromGlobal(QCursor::pos())));
	} else {
		used_rect.moveCenter(QPointF(0.0, 0.0));
	}
	m_scene -> getPasteArea(used_rect);
}

/**
	Slot appele lorsque la scene annonce avoir defini une zone de collage
	@param target_rect Rectangle cible pour le collage
*/
ElementContent ElementView::pasteAreaDefined(const QRectF &target_rect) {
	if (to_paste_in_area_.isEmpty()) return(ElementContent());

	QDomDocument xml_document;
	if (!xml_document.setContent(to_paste_in_area_)) {
		to_paste_in_area_.clear();
		return(ElementContent());
	} else {
		return(paste(xml_document, target_rect.topLeft()));
	}
}

/**
	Colle le document XML xml_document a la position pos
	@param xml_document Document XML a coller
	@param pos Coin superieur gauche du rectangle cible
*/
ElementContent ElementView::paste(const QDomDocument &xml_document, const QPointF &pos) {
	// objet pour recuperer le contenu ajoute au schema par le coller
	ElementContent content_pasted;
	m_scene -> fromXml(xml_document, pos, false, &content_pasted);

	// si quelque chose a effectivement ete ajoute au schema, on cree un objet d'annulation
	if (content_pasted.count()) {
		m_scene -> clearSelection();
		PastePartsCommand *undo_object = new PastePartsCommand(this, content_pasted);
		m_scene -> undoStack().push(undo_object);
	}
	return(content_pasted);
}

/**
	Colle le document XML xml_document a la position pos
	@param xml_document Document XML a coller
*/
ElementContent ElementView::pasteWithOffset(const QDomDocument &xml_document) {
	// objet pour recuperer le contenu ajoute au schema par le coller
	ElementContent content_pasted;

	// rectangle source
	QRectF pasted_content_bounding_rect = m_scene -> boundingRectFromXml(xml_document);
	if (pasted_content_bounding_rect.isEmpty()) return(content_pasted);

	// copier/coller avec decalage
	QRectF final_pasted_content_bounding_rect;
	++ offset_paste_count_;
	if (!offset_paste_count_) {
		// the pasted content was cut
		start_top_left_corner_ = pasted_content_bounding_rect.topLeft();
		final_pasted_content_bounding_rect = pasted_content_bounding_rect;
	}
	else {
		// the pasted content was copied
		if (offset_paste_count_ == 1) {
			start_top_left_corner_ = pasted_content_bounding_rect.topLeft();
		} else {
			pasted_content_bounding_rect.moveTopLeft(start_top_left_corner_);
		}

		// on applique le decalage qui convient
		final_pasted_content_bounding_rect = applyMovement(
			pasted_content_bounding_rect,
			QETElementEditor::pasteOffset()
		);
	}
	QPointF old_start_top_left_corner = start_top_left_corner_;
	start_top_left_corner_ = final_pasted_content_bounding_rect.topLeft();
	m_scene -> fromXml(xml_document, start_top_left_corner_, false, &content_pasted);

	// si quelque chose a effectivement ete ajoute au schema, on cree un objet d'annulation
	if (content_pasted.count()) {
		m_scene -> clearSelection();
		PastePartsCommand *undo_object = new PastePartsCommand(this, content_pasted);
		undo_object -> setOffset(offset_paste_count_ - 1, old_start_top_left_corner, offset_paste_count_, start_top_left_corner_);
		m_scene -> undoStack().push(undo_object);
	}
	return(content_pasted);
}

/**
	Gere les clics sur la vue - permet de coller lorsaue l'on enfonce le bouton
	du milieu de la souris.
	@param e QMouseEvent decrivant l'evenement souris
*/
void ElementView::mousePressEvent(QMouseEvent* e)
{
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 1) // ### Qt 6: remove
	if (e->button() == Qt::MidButton)
#else
#if TODO_LIST
#pragma message("@TODO remove code for QT 6 or later")
#endif
	if (e->button() == Qt::MiddleButton)
#endif
	{
		setCursor( (Qt::ClosedHandCursor));
		reference_view_ = e->pos();
	}
	else
		QGraphicsView::mousePressEvent(e);
}

/**
	@brief ElementView::mouseMoveEvent
	Manage the event move mouse
*/
void ElementView::mouseMoveEvent(QMouseEvent* e)
{
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 1) // ### Qt 6: remove
	if (e->buttons() == Qt::MidButton)
#else
#if TODO_LIST
#pragma message("@TODO remove code for QT 6 or later")
#endif
	if (e->buttons() == Qt::MiddleButton)
#endif
	{
		QScrollBar *h = horizontalScrollBar();
		QScrollBar *v = verticalScrollBar();
		QPointF pos = reference_view_ - e -> pos();
		reference_view_ = e -> pos();
		h -> setValue(h -> value() + pos.x());
		v -> setValue(v -> value() + pos.y());
	}
	else
		QGraphicsView::mouseMoveEvent(e);
}

/**
	@brief ElementView::mouseReleaseEvent
	Manage event release click mouse
*/
void ElementView::mouseReleaseEvent(QMouseEvent* e)
{
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 1) // ### Qt 6: remove
	if (e->button() == Qt::MidButton)
#else
#if TODO_LIST
#pragma message("@TODO remove code for QT 6 or later")
#endif
	if (e->button() == Qt::MiddleButton)
#endif
	{
		setCursor(Qt::ArrowCursor);
		adjustSceneRect();
		return;
	}
	QGraphicsView::mouseReleaseEvent(e);
}

/**
	@brief ElementView::gestures
	@return
*/
bool ElementView::gestures() const
{
	QSettings settings;
	return(settings.value("diagramview/gestures", false).toBool());
}


/**
	@brief ElementView::wheelEvent
	@param e
*/
void ElementView::wheelEvent(QWheelEvent *e) {
	//Zoom and scrolling
	if ( gestures() ) {
		if (e -> modifiers() & Qt::ControlModifier)
			e -> angleDelta().y() > 0 ? zoomInSlowly() : zoomOutSlowly();
		else
			QGraphicsView::wheelEvent(e);
	} else {
		e -> angleDelta().y() > 0 ? zoomIn(): zoomOut();
	}
}

/**
	Gere les evenements de la ElementView
	@param e Evenement
*/
bool ElementView::event(QEvent *e) {
	// By default touch events are converted to mouse events. So
	// after this event we will get a mouse event also but we want
	// to handle touch events as gestures only. So we need this safeguard
	// to block mouse events that are actually generated from touch.
	if (e->type() == QEvent::Gesture)
		return gestureEvent(static_cast<QGestureEvent *>(e));

	return(QGraphicsView::event(e));
}

/**
	Utilise le pincement du trackpad pour zoomer
	@brief ElementView::gestureEvent
	@param event
	@return
*/
bool ElementView::gestureEvent(QGestureEvent *event){
	if (QGesture *gesture = event->gesture(Qt::PinchGesture)) {
		QPinchGesture *pinch = static_cast<QPinchGesture *>(gesture);
		if (pinch->changeFlags() & QPinchGesture::ScaleFactorChanged){
			qreal value = gesture->property("scaleFactor").toReal();
			if (value > 1){
				zoomInSlowly();
			}else{
				zoomOutSlowly();
			}
		}
	}
	return true;
}


/**
	Dessine l'arriere-plan de l'editeur, cad la grille.
	@param p Le QPainter a utiliser pour dessiner
	@param r Le rectangle de la zone a dessiner
*/
void ElementView::drawBackground(QPainter *p, const QRectF &r) {
	p -> save();

	// desactive tout antialiasing, sauf pour le texte
	p -> setRenderHint(QPainter::Antialiasing, false);
	p -> setRenderHint(QPainter::TextAntialiasing, true);
	p -> setRenderHint(QPainter::SmoothPixmapTransform, false);

	// dessine un fond blanc
	p -> setPen(Qt::NoPen);
	p -> setBrush(Qt::white);
	p -> drawRect(r);

	// determine le zoom en cours
	qreal zoom_factor = transform().m11();

	// choisit la granularite de la grille en fonction du zoom en cours
	int drawn_x_grid = 1;//scene_ -> xGrid();
	int drawn_y_grid = 1;//scene_ -> yGrid();
	bool draw_grid = true;
	bool draw_cross = false;

	if (zoom_factor < (4.0/3.0)) { //< no grid
		draw_grid = false;
	} else if (zoom_factor < 4.0) { //< grid 10*10
		drawn_x_grid *= 10;
		drawn_y_grid *= 10;
	}else if (zoom_factor < 8.0) { //< grid 5*5
		drawn_x_grid *= 5;
		drawn_y_grid *= 5;
		draw_cross = true;
	} else if (zoom_factor < 10.0) { //< grid 2*2
		drawn_x_grid *= 2;
		drawn_y_grid *= 2;
		draw_cross = true;
	} else { //< grid 1*1
		draw_cross = true;
	}

	m_scene->setGrid(drawn_x_grid, drawn_y_grid);

	if (draw_grid) {
		// draw the dot of the grid
		QPen pen(Qt::black);
		pen.setCosmetic(true);
		p -> setPen(pen);
		p -> setBrush(Qt::NoBrush);
		qreal limite_x = r.x() + r.width();
		qreal limite_y = r.y() + r.height();

		int g_x = (int)ceil(r.x());
		while (g_x % drawn_x_grid) ++ g_x;
		int g_y = (int)ceil(r.y());
		while (g_y % drawn_y_grid) ++ g_y;

		for (int gx = g_x ; gx < limite_x ; gx += drawn_x_grid) {
			for (int gy = g_y ; gy < limite_y ; gy += drawn_y_grid) {
				if (draw_cross) {
					if (!(gx % 10) && !(gy % 10)) {
						p -> drawLine(QLineF(gx - 0.25, gy, gx + 0.25, gy));
						p -> drawLine(QLineF(gx, gy - 0.25, gx, gy + 0.25));
					} else {
						p -> drawPoint(gx, gy);
					}
				} else {
					p -> drawPoint(gx, gy);
				}
			}
		}
	}
	p -> restore();
}

/**
	@brief ElementView::applyMovement
	Applique le decalage offset dans le sens movement au rectangle start
	@param start :
	rectangle a decaler
	@param offset :
	Decalage a appliquer
	@return
*/
QRectF ElementView::applyMovement(const QRectF &start, const QPointF &offset) {
	// calcule le decalage a appliquer a partir de l'offset
	QPointF final_offset;
	final_offset.rx() =  start.width() + offset.x();

	// applique le decalage ainsi calcule
	return(start.translated(final_offset));
}
