#include "zoomableimage.hpp"

#include <algorithm>

#include <QHBoxLayout>
#include <QAction>
#include <QMenu>
#include <QFileDialog>
#include <QPixmap>

#include "util.hpp"
#include "types.hpp"

#include <iostream>
#include <sstream>

/**
 * @brief Puts a value into a stringstream. (used to print char and uchar as a
 * value instead a char.
 * @param ss The stringstream.
 * @param val The value.
 */
template <int depth>
void putInStream(std::stringstream &ss,
		 const cvv::qtutil::DepthType<depth> &val)
{
	ss << val;
}

/**
 * @brief Puts a value into a stringstream. (used to print char and uchar as a
 * value instead a char.
 * @param ss The stringstream.
 * @param val The value.
 */
template <>
void putInStream<CV_8U>(std::stringstream &ss,
			const cvv::qtutil::DepthType<CV_8U> &val)
{
	ss << static_cast<cvv::qtutil::DepthType<CV_16S>>(val);
}

/**
 * @brief Puts a value into a stringstream. (used to print char and uchar as a
 * value instead a char.
 * @param ss The stringstream.
 * @param val The value.
 */
template <>
void putInStream<CV_8S>(std::stringstream &ss,
			const cvv::qtutil::DepthType<CV_8S> &val)
{
	ss << static_cast<cvv::qtutil::DepthType<CV_16S>>(val);
}

/**
 * @brief Returns the channels of pixel mat,col from mat as a string.
 * @param mat The mat.
 * @param col The col.
 * @param row The row.
 * @return The channels of pixel mat,col from mat as a string.
 */
template <int depth, int channels>
std::string printPixel(const cv::Mat &mat, int spalte, int zeile)
{
	std::stringstream ss{};
	typedef cv::Vec<cvv::qtutil::DepthType<depth>, channels> VecType;
	const VecType &p = mat.at<VecType>(zeile, spalte);

	putInStream<depth>(ss, p[0]);
	for (int c = 1; c < mat.channels(); c++)
	{
		ss << "\n";
		putInStream<depth>(ss, p[c]);
	}
	return ss.str();
}

/**
 * @brief Returns the channels of pixel mat,col from mat as a string.
 * (This step spilts at the number of channels)
 * @param mat The mat.
 * @param i The col.
 * @param j The row.
 * @return The channels of pixel mat,col from mat as a string. (or ">6
 * channels")
 */
template <int depth> std::string printPixel(const cv::Mat &mat, int i, int j)
{
	if (mat.channels() < 1)
	{
		return "<1 channel";
	}
	switch (mat.channels())
	{
	case 1:
		return printPixel<depth, 1>(mat, i, j);
		break;
	case 2:
		return printPixel<depth, 2>(mat, i, j);
		break;
	case 3:
		return printPixel<depth, 3>(mat, i, j);
		break;
	case 4:
		return printPixel<depth, 4>(mat, i, j);
		break;
	case 5:
		return printPixel<depth, 5>(mat, i, j);
		break;
	case 6:
		return printPixel<depth, 6>(mat, i, j);
		break;
	case 7:
		return printPixel<depth, 7>(mat, i, j);
		break;
	case 8:
		return printPixel<depth, 8>(mat, i, j);
		break;
	case 9:
		return printPixel<depth, 9>(mat, i, j);
		break;
	case 10:
		return printPixel<depth, 10>(mat, i, j);
		break;
	default:
		return ">10 channels";
	}
}

/**
 * @brief Returns the channels of pixel mat,col from mat as a string.
 * (This step spilts at the depth)
 * @param mat The mat.
 * @param i The col.
 * @param j The row.
 * @return The channels of pixel mat,col from mat as a string. (or ">6
 * channels")
 */
static std::string printPixel(const cv::Mat &mat, int i, int j)
{
	if (i >= 0 && j >= 0)
	{
		if (i < mat.cols && j < mat.rows)
		{
			switch (mat.depth())
			{
			case CV_8U:
				return printPixel<CV_8U>(mat, i, j);
				break;
			case CV_8S:
				return printPixel<CV_8S>(mat, i, j);
				break;
			case CV_16U:
				return printPixel<CV_16U>(mat, i, j);
				break;
			case CV_16S:
				return printPixel<CV_16S>(mat, i, j);
				break;
			case CV_32S:
				return printPixel<CV_32S>(mat, i, j);
				break;
			case CV_32F:
				return printPixel<CV_32F>(mat, i, j);
				break;
			case CV_64F:
				return printPixel<CV_64F>(mat, i, j);
				break;
			}
			return "unknown depth";
		}
	}
	return "";
}

namespace cvv
{
namespace qtutil
{
ZoomableImage::ZoomableImage(const cv::Mat &mat, QWidget *parent)
    : QWidget{ parent }, mat_{ mat }, pixmap_{ nullptr }, view_{ nullptr },
      scene_{ nullptr }, zoom_{ 1 }, threshold_{ 60 }, autoShowValues_{ true },
      values_{}, scrollFactorCTRL_{ 1.025 }, scrollFactorCTRLShift_{ 1.01 },
      updateAreaTimer_{}, updateAreaQueued_{false}, updateAreaDelay_{50}
{
	// qt5 doc : "The view does not take ownership of scene."
	auto scene = util::make_unique<QGraphicsScene>(this);
	scene_ = *scene;

	auto view = util::make_unique<structures::ZoomableImageGraphicsView>();
	view_ = *view;
	view_->setScene(scene.release());

	QObject::connect((view_->horizontalScrollBar()),
			 &QScrollBar::valueChanged, this,
			 &ZoomableImage::viewScrolled);
	QObject::connect((view_->verticalScrollBar()),
			 &QScrollBar::valueChanged, this,
			 &ZoomableImage::viewScrolled);
	QObject::connect(this, SIGNAL(updateArea(QRectF, qreal)), this,
			 SLOT(drawValues()));
	// scrollbars should have strong focus
	view_->horizontalScrollBar()->setFocusPolicy(Qt::FocusPolicy::NoFocus);
	view_->verticalScrollBar()->setFocusPolicy(Qt::NoFocus);
	view_->setFocusPolicy(Qt::NoFocus);
	auto layout = util::make_unique<QHBoxLayout>();
	layout->addWidget(view.release());
	layout->setContentsMargins(QMargins());
	setLayout(layout.release());
	setMat(mat_);
	// rightklick
	setContextMenuPolicy(Qt::CustomContextMenu);
	QObject::connect(this,
			 SIGNAL(customContextMenuRequested(const QPoint &)),
			 this, SLOT(rightClick(QPoint)));
	//update area timer
	updateAreaTimer_.setSingleShot(true);
	QObject::connect(&updateAreaTimer_,SIGNAL(timeout()),this,SLOT(emitUpdateArea()));

	showFullImage();
	setMouseTracking(true);
}

void ZoomableImage::setMat(cv::Mat mat)
{
	mat_ = mat;
	auto result = convertMatToQPixmap(mat_);
	emit updateConversionResult( mat,result.first);
	lastConversionResult_=result.first;
	// QTReference:
	// void QGraphicsScene::clear() [slot]
	// Removes and deletes all items from the scene, but
	// otherwise leaves the state of the scene unchanged.
	//=>pixmap+values are deleted
	scene_->clear();
	pixmap_ = *(scene_->addPixmap(result.second));
	values_.clear();

	drawValues();
}

void ZoomableImage::setZoom(qreal factor)
{
	if (factor <= 0)
	{
		return;
	}
	qreal nscale = factor / zoom_;
	zoom_ = factor;
	view_->scale(nscale, nscale);
	//less signals
	queueUpdateArea();
}

void ZoomableImage::queueUpdateArea()
{
	if(!updateAreaQueued_)
	{
		updateAreaQueued_=true;
		updateAreaTimer_.start(updateAreaDelay_);
	}
}

void ZoomableImage::drawValues()
{
	// delete old values
	for (auto &elem : values_)
	{
		scene_->removeItem(elem);
		// QGraphicsItem has no delete later
		delete elem;
	}
	values_.clear();
	// draw values?
	if (!(autoShowValues_ && (zoom_ >= threshold_)))
	{
		return;
	}
	auto r = visibleArea();

	for (int j = std::max(0, static_cast<int>(r.top()) - 1);
	     j < std::min(mat_.rows, static_cast<int>(r.bottom()) + 1); j++)
	{
		for (int i = std::max(0, static_cast<int>(r.left()) - 1);
		     i < std::min(mat_.cols, static_cast<int>(r.right()) + 1);
		     i++)
		{
			QString s(printPixel(mat_, i, j).c_str());

			s.replace('\n', "<br>");
			QGraphicsTextItem *txt = scene_->addText("");
			txt->setHtml(
			    QString("<div style='background-color:rgba(255, "
				    "255, 255, 0.5);'>") +
			    s + "</div>");
			txt->setPos(i, j);
			txt->setScale(0.008);
			values_.push_back(txt);
		}
	}
}

void ZoomableImage::wheelEvent(QWheelEvent *event)
{

	if (QApplication::keyboardModifiers() & Qt::ControlModifier)
	{
		qreal f = scrollFactorCTRL_;
		;
		if (QApplication::keyboardModifiers() & Qt::ShiftModifier)
		{
			f = scrollFactorCTRLShift_;
			;
		}

		qreal scroll =
		    ((event->angleDelta().x()) + (event->angleDelta().y())) * f;
		if (scroll < 0.0)
		{
			scroll = -1.0 / scroll;
			f = 1 / f;
		}
		setZoom(f * zoom_);
	}
	else
	{
		QWidget::wheelEvent(event);
	}
}

void ZoomableImage::setArea(QRectF rect, qreal zoom)
{
	setZoom(zoom);
	view_->centerOn(rect.topLeft() +
			(rect.bottomRight() - rect.topLeft()) / 2);
}

void ZoomableImage::showFullImage()
{
	qreal iw = static_cast<qreal>(imageWidth());
	qreal ih = static_cast<qreal>(imageHeight());
	if (!((iw != 0) && (ih != 0)))
	{
		return;
	}
	setZoom(std::min(static_cast<qreal>(view_->viewport()->width()) / iw,
			 static_cast<qreal>(view_->viewport()->height()) / ih));
}

QRectF ZoomableImage::visibleArea() const
{
	QRectF result{};
	result.setTopLeft(view_->mapToScene(QPoint{ 0, 0 }));
	result.setBottomRight(view_->mapToScene(
	    QPoint{ view_->viewport()->width(), view_->viewport()->height() }));
	return result;
}

void ZoomableImage::rightClick(const QPoint &pos)
{
	QPoint p = mapToGlobal(pos);
	QMenu menu;

	menu.addAction("Save orginal image");
	menu.addAction("Save visible image");

	QAction *item = menu.exec(p);
	if (item)
	{
		QString fileName = QFileDialog::getSaveFileName(
		    this, tr("Save File"), ".",
		    tr("BMP (*.bmp);;GIF (*.gif);;JPG (*.jpg);;PNG (*.png);;"
		       "PBM (*.pbm);;PGM (*.pgm);;PPM (*.ppm);;XBM (*.xbm);;"
		       "XPM (*.xpm)"));
		if (fileName == "")
		{
			return;
		}
		QPixmap pmap;
		if ((item->text()) == "Save orginal image")
		{
			pmap = fullImage();
		}
		else
		{
			pmap = visibleImage();
		}

		pmap.save(fileName, 0, 100);
	}
}

QPointF ZoomableImage::mapImagePointToParent(QPointF point) const
{
	return mapToParent(view_->mapToParent(
	    view_->mapFromScene(pixmap_->mapToScene(point))));
}

void ZoomableImage::mouseMoveEvent(QMouseEvent * event)
{
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
	QPoint pos = event->globalPosition().toPoint();
#else
	QPoint pos = event->globalPos();
#endif

	QPointF imgPos=view_->mapToScene(view_->mapFromGlobal(pos));
	bool inImage=(imgPos.x()>=0)
			&&(imgPos.y()>=0)
			&&(imgPos.x()<=imageWidth())
			&&(imgPos.y()<=imageHeight());
	QString pixelVal{""};
	if(inImage)
	{
		pixelVal=QString{printPixel(mat_, imgPos.x(), imgPos.y()).c_str()};
	}
	emit updateMouseHover(imgPos,pixelVal,inImage);
}

void ZoomableImage::emitUpdateArea()
{
	updateAreaQueued_=false;
	emit updateArea(visibleArea(),zoom_);
}

namespace structures
{

void ZoomableImageGraphicsView::wheelEvent(QWheelEvent *event)
{
	if (QApplication::keyboardModifiers() & Qt::ControlModifier)
	{
		event->ignore();
	}
	else
	{
		QGraphicsView::wheelEvent(event);
	}
}
}
}
}
