/*
    File                 : Image.cpp
    Project              : LabPlot
    Description          : Worksheet element to draw images
    --------------------------------------------------------------------
    SPDX-FileCopyrightText: 2019 Alexander Semke <alexander.semke@web.de>

    SPDX-License-Identifier: GPL-2.0-or-later
*/

#include "Image.h"
#include "Worksheet.h"
#include "ImagePrivate.h"
#include "backend/lib/commandtemplates.h"
#include "backend/lib/XmlStreamReader.h"
#include "backend/worksheet/plots/PlotArea.h"

#include <QBuffer>
#include <QFileInfo>
#include <QGraphicsScene>
#include <QGraphicsSceneMouseEvent>
#include <QIcon>
#include <QMenu>
#include <QPainter>

#include <KConfig>
#include <KConfigGroup>
#include <KLocalizedString>

/**
 * \class Image
 * \brief A label supporting rendering of html- and tex-formatted texts.
 *
 * The label is aligned relative to the specified position.
 * The position can be either specified by providing the x- and y- coordinates
 * in parent's coordinate system, or by specifying one of the predefined position
 * flags (\c HorizontalPosition, \c VerticalPosition).
 */


Image::Image(const QString& name)
	: WorksheetElement(name, new ImagePrivate(this), AspectType::Image) {

	init();
}

Image::Image(const QString &name, ImagePrivate *dd)
	: WorksheetElement(name, dd, AspectType::Image) {

	init();
}

void Image::init() {
	Q_D(Image);

	KConfig config;
	KConfigGroup group = config.group("Image");

	d->embedded = group.readEntry("embedded", true);
	d->opacity = group.readEntry("opacity", d->opacity);

	//geometry
	d->position.point.setX(group.readEntry("PositionXValue", 0.));
	d->position.point.setY(group.readEntry("PositionYValue", 0.));
	d->position.horizontalPosition = (WorksheetElement::HorizontalPosition) group.readEntry("PositionX", (int)WorksheetElement::HorizontalPosition::Center);
	d->position.verticalPosition = (WorksheetElement::VerticalPosition) group.readEntry("PositionY", (int)WorksheetElement::VerticalPosition::Center);
	d->horizontalAlignment = (WorksheetElement::HorizontalAlignment) group.readEntry("HorizontalAlignment", (int)WorksheetElement::HorizontalAlignment::Center);
	d->verticalAlignment = (WorksheetElement::VerticalAlignment) group.readEntry("VerticalAlignment", (int)WorksheetElement::VerticalAlignment::Center);
	d->rotationAngle = group.readEntry("Rotation", d->rotationAngle);

	//border
	d->borderPen = QPen(group.readEntry("BorderColor", d->borderPen.color()),
						group.readEntry("BorderWidth", d->borderPen.widthF()),
						(Qt::PenStyle) group.readEntry("BorderStyle", (int)(d->borderPen.style())));
	d->borderOpacity = group.readEntry("BorderOpacity", d->borderOpacity);
}

//no need to delete the d-pointer here - it inherits from QGraphicsItem
//and is deleted during the cleanup in QGraphicsScene
Image::~Image() = default;

QGraphicsItem* Image::graphicsItem() const {
	return d_ptr;
}

void Image::setParentGraphicsItem(QGraphicsItem* item) {
	Q_D(Image);
	d->setParentItem(item);
	d->updatePosition();
}

void Image::retransform() {
	Q_D(Image);
	d->retransform();
}

void Image::handleResize(double /*horizontalRatio*/, double /*verticalRatio*/, bool /*pageResize*/) {
	DEBUG(Q_FUNC_INFO);
// 	Q_D(Image);

// 	double ratio = 0;
// 	if (horizontalRatio > 1.0 || verticalRatio > 1.0)
// 		ratio = qMax(horizontalRatio, verticalRatio);
// 	else
// 		ratio = qMin(horizontalRatio, verticalRatio);
}

/*!
	Returns an icon to be used in the project explorer.
*/
QIcon Image::icon() const{
	return QIcon::fromTheme("viewimage");
}

QMenu* Image::createContextMenu() {
	QMenu *menu = WorksheetElement::createContextMenu();
	QAction* firstAction = menu->actions().at(1); //skip the first action because of the "title-action"

	if (!visibilityAction) {
		visibilityAction = new QAction(i18n("Visible"), this);
		visibilityAction->setCheckable(true);
		connect(visibilityAction, &QAction::triggered, this, &Image::changeVisibility);
	}

	visibilityAction->setChecked(isVisible());
	menu->insertAction(firstAction, visibilityAction);
	menu->insertSeparator(firstAction);

	return menu;
}

/* ============================ getter methods ================= */
BASIC_SHARED_D_READER_IMPL(Image, QString, fileName, fileName)
BASIC_SHARED_D_READER_IMPL(Image, bool, embedded, embedded)
BASIC_SHARED_D_READER_IMPL(Image, qreal, opacity, opacity)
BASIC_SHARED_D_READER_IMPL(Image, int, width, width)
BASIC_SHARED_D_READER_IMPL(Image, int, height, height)
BASIC_SHARED_D_READER_IMPL(Image, bool, keepRatio, keepRatio)

BASIC_SHARED_D_READER_IMPL(Image, QPen, borderPen, borderPen)
BASIC_SHARED_D_READER_IMPL(Image, qreal, borderOpacity, borderOpacity)

/* ============================ setter methods and undo commands ================= */
STD_SETTER_CMD_IMPL_F_S(Image, SetFileName, QString, fileName, updateImage)
void Image::setFileName(const QString& fileName) {
	Q_D(Image);
	if (fileName != d->fileName)
		exec(new ImageSetFileNameCmd(d, fileName, ki18n("%1: set image")));
}

STD_SETTER_CMD_IMPL_S(Image, SetEmbedded, bool, embedded)
void Image::setEmbedded(bool embedded) {
	Q_D(Image);
	if (embedded != d->embedded)
		exec(new ImageSetEmbeddedCmd(d, embedded, ki18n("%1: embed image")));
}

STD_SETTER_CMD_IMPL_F_S(Image, SetOpacity, qreal, opacity, update)
void Image::setOpacity(qreal opacity) {
	Q_D(Image);
	if (opacity != d->opacity)
		exec(new ImageSetOpacityCmd(d, opacity, ki18n("%1: set border opacity")));
}

STD_SETTER_CMD_IMPL_F_S(Image, SetWidth, int, width, retransform)
void Image::setWidth(int width) {
	Q_D(Image);
	if (width != d->width) {
		exec(new ImageSetWidthCmd(d, width, ki18n("%1: set width")));
		d->scaleImage();
	}
}

STD_SETTER_CMD_IMPL_F_S(Image, SetHeight, int, height, retransform)
void Image::setHeight(int height) {
	Q_D(Image);
	if (height != d->height) {
		exec(new ImageSetHeightCmd(d, height, ki18n("%1: set height")));
		d->scaleImage();
	}
}

STD_SETTER_CMD_IMPL_S(Image, SetKeepRatio, bool, keepRatio)
void Image::setKeepRatio(bool keepRatio) {
	Q_D(Image);
	if (keepRatio != d->keepRatio)
		exec(new ImageSetKeepRatioCmd(d, keepRatio, ki18n("%1: change keep ratio")));
}

//Border
STD_SETTER_CMD_IMPL_F_S(Image, SetBorderPen, QPen, borderPen, update)
void Image::setBorderPen(const QPen &pen) {
	Q_D(Image);
	if (pen != d->borderPen)
		exec(new ImageSetBorderPenCmd(d, pen, ki18n("%1: set border")));
}

STD_SETTER_CMD_IMPL_F_S(Image, SetBorderOpacity, qreal, borderOpacity, update)
void Image::setBorderOpacity(qreal opacity) {
	Q_D(Image);
	if (opacity != d->borderOpacity)
		exec(new ImageSetBorderOpacityCmd(d, opacity, ki18n("%1: set border opacity")));
}

//##############################################################################
//####################### Private implementation ###############################
//##############################################################################
ImagePrivate::ImagePrivate(Image* owner) : WorksheetElementPrivate(owner), q(owner) {
	setFlag(QGraphicsItem::ItemIsSelectable);
	setFlag(QGraphicsItem::ItemIsMovable);
	setFlag(QGraphicsItem::ItemSendsGeometryChanges);
	setFlag(QGraphicsItem::ItemIsFocusable);
	setAcceptHoverEvents(true);

	//initial placeholder image
	image = QIcon::fromTheme("viewimage").pixmap(width, height).toImage();
	m_image = image;
}

/*!
	calculates the position and the bounding box of the label. Called on geometry or text changes.
 */
void ImagePrivate::retransform() {
	if (suppressRetransform || q->isLoading())
		return;

	int w = m_image.width();
	int h = m_image.height();
	boundingRectangle.setX(-w/2);
	boundingRectangle.setY(-h/2);
	boundingRectangle.setWidth(w);
	boundingRectangle.setHeight(h);

	updatePosition();
	updateBorder();

	Q_EMIT q->changed();
}

void ImagePrivate::updateImage() {
	if (!fileName.isEmpty()) {
		image = QImage(fileName);
		width = image.width();
		height = image.height();
	} else {
		width = Worksheet::convertToSceneUnits(2, Worksheet::Unit::Centimeter);
		height = Worksheet::convertToSceneUnits(3, Worksheet::Unit::Centimeter);
		image = QIcon::fromTheme("viewimage").pixmap(width, height).toImage();
	}

	m_image = image;

	Q_EMIT q->widthChanged(width);
	Q_EMIT q->heightChanged(height);

	retransform();
}

void ImagePrivate::scaleImage() {
	if (m_image.isNull())
		m_image = image; //initial call from load(), m_image not initialized yet

	if (keepRatio) {
		if (width != m_image.width()) {
			//width was changed -> rescale the height to keep the ratio
			if (m_image.width() != 0)
				height = m_image.height()*width/m_image.width();
			else
				height = 0;
			Q_EMIT q->heightChanged(height);
		} else if (height != m_image.height()) {
			//height was changed -> rescale the width to keep the ratio
			if (m_image.height() != 0)
				width = m_image.width()*height/m_image.height();
			else
				width = 0;
			Q_EMIT q->widthChanged(width);
		}
	}

	if (width != 0 && height != 0)
		m_image = image.scaled(width, height, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);

	retransform();
}

void ImagePrivate::updateBorder() {
	borderShapePath = QPainterPath();
	borderShapePath.addRect(boundingRectangle);
	recalcShapeAndBoundingRect();
}

/*!
	Returns the outer bounds of the item as a rectangle.
 */
QRectF ImagePrivate::boundingRect() const {
	return transformedBoundingRectangle;
}

/*!
	Returns the shape of this item as a QPainterPath in local coordinates.
*/
QPainterPath ImagePrivate::shape() const {
	return imageShape;
}

/*!
  recalculates the outer bounds and the shape of the label.
*/
void ImagePrivate::recalcShapeAndBoundingRect() {
	prepareGeometryChange();

	QMatrix matrix;
	matrix.rotate(-rotationAngle);
	imageShape = QPainterPath();
	if (borderPen.style() != Qt::NoPen) {
		imageShape.addPath(WorksheetElement::shapeFromPath(borderShapePath, borderPen));
		transformedBoundingRectangle = matrix.mapRect(imageShape.boundingRect());
	} else {
		imageShape.addRect(boundingRectangle);
		transformedBoundingRectangle = matrix.mapRect(boundingRectangle);
	}

	imageShape = matrix.map(imageShape);

	Q_EMIT q->changed();
}

void ImagePrivate::paint(QPainter* painter, const QStyleOptionGraphicsItem* /*option*/, QWidget*) {
	painter->save();

	//draw the image
	painter->rotate(-rotationAngle);
	painter->setOpacity(opacity);
	painter->drawImage(boundingRectangle.topLeft(), m_image, m_image.rect());
	painter->restore();

	//draw the border
	if (borderPen.style() != Qt::NoPen) {
		painter->save();
		painter->rotate(-rotationAngle);
		painter->setPen(borderPen);
		painter->setBrush(Qt::NoBrush);
		painter->setOpacity(borderOpacity);
		painter->drawPath(borderShapePath);
		painter->restore();
	}

	if (m_hovered && !isSelected() && !q->isPrinting()) {
		painter->setPen(QPen(QApplication::palette().color(QPalette::Shadow), 2, Qt::SolidLine));
		painter->drawPath(imageShape);
	}

	if (isSelected() && !q->isPrinting()) {
		painter->setPen(QPen(QApplication::palette().color(QPalette::Highlight), 2, Qt::SolidLine));
		painter->drawPath(imageShape);
	}
}

void ImagePrivate::contextMenuEvent(QGraphicsSceneContextMenuEvent* event) {
	q->createContextMenu()->exec(event->screenPos());
}

void ImagePrivate::hoverEnterEvent(QGraphicsSceneHoverEvent*) {
	if (!isSelected()) {
		m_hovered = true;
		Q_EMIT q->hovered();
		update();
	}
}

void ImagePrivate::hoverLeaveEvent(QGraphicsSceneHoverEvent*) {
	if (m_hovered) {
		m_hovered = false;
		Q_EMIT q->unhovered();
		update();
	}
}

//##############################################################################
//##################  Serialization/Deserialization  ###########################
//##############################################################################
//! Save as XML
void Image::save(QXmlStreamWriter* writer) const {
	Q_D(const Image);

	writer->writeStartElement("image");
	writeBasicAttributes(writer);
	writeCommentElement(writer);

	//general
	writer->writeStartElement("general");
	if (d->embedded) {
		QFileInfo fi(d->fileName);
		writer->writeAttribute("fileName", fi.fileName()); //save the actual file name only and not the whole path
	} else
		writer->writeAttribute("fileName", d->fileName);

	writer->writeAttribute("embedded", QString::number(d->embedded));
	writer->writeAttribute("opacity", QString::number(d->opacity));
	writer->writeEndElement();

	//image data
	if (d->embedded && !d->image.isNull()) {
		writer->writeStartElement("data");
		QByteArray data;
		QBuffer buffer(&data);
		buffer.open(QIODevice::WriteOnly);
		d->image.save(&buffer, "PNG");
		writer->writeCharacters(data.toBase64());
		writer->writeEndElement();
	}

	//geometry
	writer->writeStartElement("geometry");
	WorksheetElement::save(writer);
	writer->writeAttribute("width", QString::number(d->width));
	writer->writeAttribute("height", QString::number(d->height));
	writer->writeAttribute("keepRatio", QString::number(d->keepRatio));
	writer->writeEndElement();

	//border
	writer->writeStartElement("border");
	WRITE_QPEN(d->borderPen);
	writer->writeAttribute("borderOpacity", QString::number(d->borderOpacity));
	writer->writeEndElement();

	writer->writeEndElement(); // close "image" section
}

//! Load from XML
bool Image::load(XmlStreamReader* reader, bool preview) {
	if (!readBasicAttributes(reader))
		return false;

	Q_D(Image);
	KLocalizedString attributeWarning = ki18n("Attribute '%1' missing or empty, default value is used");
	QXmlStreamAttributes attribs;
	QString str;

	while (!reader->atEnd()) {
		reader->readNext();
		if (reader->isEndElement() && reader->name() == "image")
			break;

		if (!reader->isStartElement())
			continue;

		if (!preview && reader->name() == "comment") {
			if (!readCommentElement(reader)) return false;
		} else if (!preview && reader->name() == "general") {
			attribs = reader->attributes();
			d->fileName = attribs.value("fileName").toString();
			READ_INT_VALUE("embedded", embedded, bool);
			READ_DOUBLE_VALUE("opacity", opacity);
		} else if (reader->name() == "data") {
			QByteArray ba = QByteArray::fromBase64(reader->readElementText().toLatin1());
			if (!d->image.loadFromData(ba))
				reader->raiseWarning(i18n("Failed to read image data"));
		} else if (!preview && reader->name() == "geometry") {
			attribs = reader->attributes();

			READ_INT_VALUE("width", width, int);
			READ_INT_VALUE("height", height, int);
			READ_INT_VALUE("keepRatio", keepRatio, bool);

			WorksheetElement::load(reader, preview);
		} else if (!preview && reader->name() == "border") {
			attribs = reader->attributes();
			READ_QPEN(d->borderPen);
			READ_DOUBLE_VALUE("borderOpacity", borderOpacity);
		} else { // unknown element
			reader->raiseWarning(i18n("unknown element '%1'", reader->name().toString()));
			if (!reader->skipToEndElement()) return false;
		}
	}

	if (!preview) {
		if (!d->embedded)
			d->image = QImage(d->fileName);
		d->scaleImage();
	}

	return true;
}

//##############################################################################
//#########################  Theme management ##################################
//##############################################################################
void Image::loadThemeConfig(const KConfig&) {
}

void Image::saveThemeConfig(const KConfig&) {
}
