/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.

For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "editor/scene/scene.h"

#include "editor/scene/scene_item_canvas.h"
#include "editor/scene/scene_item_line.h"
#include "editor/scene/scene_item_sticker.h"
#include "ui/image/image_prepare.h"
#include "ui/rp_widget.h"
#include "styles/style_editor.h"

#include <QGraphicsSceneMouseEvent>

namespace Editor {
namespace {

using ItemPtr = std::shared_ptr<NumberedItem>;

class ItemEraser final : public NumberedItem {
public:
	struct Target {
		std::shared_ptr<ItemLine> item;
		QPixmap before;
	};

	ItemEraser(
		QPixmap mask,
		QPointF maskPos,
		std::vector<Target> targets)
	: _mask(std::move(mask))
	, _maskPos(maskPos)
	, _targets(std::move(targets)) {
	}

	void apply() {
		for (const auto &target : _targets) {
			target.item->applyEraser(_mask, _maskPos);
		}
	}

	void revert() {
		for (const auto &target : _targets) {
			target.item->setPixmap(target.before);
		}
	}

	QRectF boundingRect() const override {
		return QRectF();
	}

	void paint(
			QPainter *,
			const QStyleOptionGraphicsItem *,
			QWidget *) override {
	}

	bool hasState(SaveState state) const override {
		const auto &saved = (state == SaveState::Keep) ? _keeped : _saved;
		return saved.saved;
	}

	void save(SaveState state) override {
		auto &saved = (state == SaveState::Keep) ? _keeped : _saved;
		saved = {
			.saved = true,
			.status = status(),
		};
	}

	void restore(SaveState state) override {
		if (!hasState(state)) {
			return;
		}
		const auto &saved = (state == SaveState::Keep) ? _keeped : _saved;
		setStatus(saved.status);
		if (saved.status == Status::Normal) {
			apply();
		} else if (saved.status == Status::Undid) {
			revert();
		}
	}

private:
	QPixmap _mask;
	QPointF _maskPos;
	std::vector<Target> _targets;

	struct {
		bool saved = false;
		NumberedItem::Status status;
	} _saved, _keeped;
};

bool SkipMouseEvent(not_null<QGraphicsSceneMouseEvent*> event) {
	return event->isAccepted() || (event->button() == Qt::RightButton);
}

} // namespace

Scene::Scene(const QRectF &rect)
: QGraphicsScene(rect)
, _canvas(std::make_shared<ItemCanvas>())
, _lastZ(std::make_shared<float64>(9000.)) {
	QGraphicsScene::addItem(_canvas.get());
	_canvas->clearPixmap();

	_canvas->grabContentRequests(
	) | rpl::on_next([=](ItemCanvas::Content &&content) {
		if (content.clear) {
			auto mask = std::move(content.pixmap);
			if (mask.isNull()) {
				return;
			}
			const auto maskPos = content.position;
			const auto maskSize = mask.size()
				/ float64(mask.devicePixelRatio());
			const auto maskRect = QRectF(maskPos, maskSize);
			auto targets = std::vector<ItemEraser::Target>();
			const auto hits = QGraphicsScene::items(
				maskRect,
				Qt::IntersectsItemBoundingRect,
				Qt::DescendingOrder);
			for (auto *raw : hits) {
				const auto it = _itemsByPointer.find(raw);
				if (it == end(_itemsByPointer)) {
					continue;
				}
				const auto &item = it->second;
				if (!item->isNormalStatus()) {
					continue;
				}
				const auto line = std::dynamic_pointer_cast<ItemLine>(item);
				if (!line) {
					continue;
				}
				auto before = line->pixmap();
				if (!line->applyEraser(mask, maskPos)) {
					continue;
				}
				targets.push_back({
					.item = line,
					.before = std::move(before),
				});
			}
			if (!targets.empty()) {
				const auto eraser = std::make_shared<ItemEraser>(
					std::move(mask),
					maskPos,
					std::move(targets));
				addItem(eraser);
				_canvas->setZValue(++_lastLineZ);
			}
			return;
		}
		if (content.blur) {
			auto mask = std::move(content.pixmap);
			if (mask.isNull() || !_blurSource) {
				return;
			}
			const auto maskPos = content.position;
			const auto maskSize = mask.size()
				/ float64(mask.devicePixelRatio());
			const auto sourceRect = QRectF(maskPos, maskSize);
			const auto expandedRect = sourceRect.toAlignedRect().adjusted(
				-st::photoEditorBlurRadius,
				-st::photoEditorBlurRadius,
				st::photoEditorBlurRadius,
				st::photoEditorBlurRadius);
			const auto captureRect = expandedRect.intersected(
				sceneRect().toAlignedRect());
			if (captureRect.isEmpty()) {
				return;
			}
			auto source = _blurSource(captureRect);
			if (source.isNull()) {
				return;
			}
			const auto sourceDpr = source.devicePixelRatio();
			if (source.format() != QImage::Format_ARGB32_Premultiplied) {
				source = source.convertToFormat(
					QImage::Format_ARGB32_Premultiplied);
				source.setDevicePixelRatio(sourceDpr);
			}
			const auto canvasVisible = _canvas->isVisible();
			_canvas->setVisible(false);
			{
				auto p = QPainter(&source);
				render(
					&p,
					QRectF(QPointF(), QSizeF(captureRect.size())),
					QRectF(captureRect),
					Qt::IgnoreAspectRatio);
			}
			_canvas->setVisible(canvasVisible);
			auto blurred = Images::BlurLargeImage(
				std::move(source),
				st::photoEditorBlurRadius);
			if (blurred.isNull()) {
				return;
			}
			blurred.setDevicePixelRatio(sourceDpr);
			auto result = QImage(
				mask.size(),
				QImage::Format_ARGB32_Premultiplied);
			result.setDevicePixelRatio(mask.devicePixelRatio());
			result.fill(Qt::transparent);
			{
				auto p = QPainter(&result);
				p.drawImage(
					QRectF(QPointF(), maskSize),
					blurred,
					QRectF(
						sourceRect.x() - captureRect.x(),
						sourceRect.y() - captureRect.y(),
						sourceRect.width(),
						sourceRect.height()));
				p.setCompositionMode(
					QPainter::CompositionMode_DestinationIn);
				p.drawPixmap(0, 0, mask);
			}
			auto blurPixmap = QPixmap::fromImage(std::move(result));
			const auto item = std::make_shared<ItemLine>(
				std::move(blurPixmap));
			item->setPos(maskPos);
			addItem(item);
			_canvas->setZValue(++_lastLineZ);
			return;
		}
		const auto item = std::make_shared<ItemLine>(
			std::move(content.pixmap));
		item->setPos(content.position);
		addItem(item);
		_canvas->setZValue(++_lastLineZ);
	}, _lifetime);
}

void Scene::cancelDrawing() {
	_canvas->cancelDrawing();
}

void Scene::addItem(ItemPtr item) {
	if (!item) {
		return;
	}
	item->setNumber(_itemNumber++);
	QGraphicsScene::addItem(item.get());
	const auto raw = item.get();
	_items.push_back(std::move(item));
	_itemsByPointer.emplace(raw, _items.back());
	_addsItem.fire({});
}

void Scene::removeItem(not_null<QGraphicsItem*> item) {
	const auto it = ranges::find_if(_items, [&](const ItemPtr &i) {
		return i.get() == item;
	});
	if (it == end(_items)) {
		return;
	}
	removeItem(*it);
}

void Scene::removeItem(const ItemPtr &item) {
	item->setStatus(NumberedItem::Status::Removed);
	_removesItem.fire({});
}

void Scene::mousePressEvent(QGraphicsSceneMouseEvent *event) {
	QGraphicsScene::mousePressEvent(event);
	if (SkipMouseEvent(event) || !sceneRect().contains(event->scenePos())) {
		return;
	}
	_canvas->handleMousePressEvent(event);
}

void Scene::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) {
	QGraphicsScene::mouseReleaseEvent(event);
	if (SkipMouseEvent(event)) {
		return;
	}
	_canvas->handleMouseReleaseEvent(event);
}

void Scene::mouseMoveEvent(QGraphicsSceneMouseEvent *event) {
	QGraphicsScene::mouseMoveEvent(event);
	if (SkipMouseEvent(event)) {
		return;
	}
	_canvas->handleMouseMoveEvent(event);
}

void Scene::applyBrush(const QColor &color, float size, Brush::Tool tool) {
	_canvas->applyBrush(color, size, tool);
}

void Scene::setBlurSource(Fn<QImage(QRect)> source) {
	_blurSource = std::move(source);
}

rpl::producer<> Scene::addsItem() const {
	return _addsItem.events();
}

rpl::producer<> Scene::removesItem() const {
	return _removesItem.events();
}

std::vector<ItemPtr> Scene::items(
		Qt::SortOrder order) const {
	auto copyItems = _items;

	ranges::sort(copyItems, [&](ItemPtr a, ItemPtr b) {
		const auto numA = a->number();
		const auto numB = b->number();
		return (order == Qt::AscendingOrder) ? (numA < numB) : (numA > numB);
	});

	return copyItems;
}

std::shared_ptr<float64> Scene::lastZ() const {
	return _lastZ;
}

void Scene::updateZoom(float64 zoom) {
	_canvas->updateZoom(zoom);
	for (const auto &item : items()) {
		if (item->type() >= ItemBase::Type) {
			static_cast<ItemBase*>(item.get())->updateZoom(zoom);
		}
	}
}

bool Scene::hasUndo() const {
	return ranges::any_of(_items, &NumberedItem::isNormalStatus);
}

bool Scene::hasRedo() const {
	return ranges::any_of(_items, &NumberedItem::isUndidStatus);
}

void Scene::performUndo() {
	const auto filtered = items(Qt::DescendingOrder);

	const auto it = ranges::find_if(filtered, &NumberedItem::isNormalStatus);
	if (it != filtered.end()) {
		if (const auto eraser = dynamic_cast<ItemEraser*>(it->get())) {
			eraser->revert();
		}
		(*it)->setStatus(NumberedItem::Status::Undid);
	}
}

void Scene::performRedo() {
	const auto filtered = items(Qt::AscendingOrder);

	const auto it = ranges::find_if(filtered, &NumberedItem::isUndidStatus);
	if (it != filtered.end()) {
		if (const auto eraser = dynamic_cast<ItemEraser*>(it->get())) {
			eraser->apply();
		}
		(*it)->setStatus(NumberedItem::Status::Normal);
	}
}

void Scene::removeIf(Fn<bool(const ItemPtr &)> proj) {
	auto copy = std::vector<ItemPtr>();
	for (const auto &item : _items) {
		const auto toRemove = proj(item);
		if (toRemove) {
			// Scene loses ownership of an item.
			// It seems for some reason this line causes a crash. =(
			// QGraphicsScene::removeItem(item.get());
		} else {
			copy.push_back(item);
		}
	}
	_items = std::move(copy);
	_itemsByPointer.clear();
	for (const auto &item : _items) {
		_itemsByPointer.emplace(item.get(), item);
	}
}

void Scene::clearRedoList() {
	for (const auto &item : _items) {
		if (item->isUndidStatus()) {
			item->setStatus(NumberedItem::Status::Removed);
		}
	}
}

void Scene::save(SaveState state) {
	removeIf([](const ItemPtr &item) {
		return item->isRemovedStatus()
			&& !item->hasState(SaveState::Keep)
			&& !item->hasState(SaveState::Save);
	});

	for (const auto &item : _items) {
		item->save(state);
	}
	clearSelection();
	cancelDrawing();
}

void Scene::restore(SaveState state) {
	removeIf([=](const ItemPtr &item) {
		return !item->hasState(state);
	});

	for (const auto &item : _items) {
		item->restore(state);
	}
	clearSelection();
	cancelDrawing();
}

Scene::~Scene() {
	// Prevent destroying by scene of all items.
	QGraphicsScene::removeItem(_canvas.get());
	for (const auto &item : items()) {
		// Scene loses ownership of an item.
		QGraphicsScene::removeItem(item.get());
	}
}

} // namespace Editor
