/*
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 "boxes/choose_filter_box.h"

#include "apiwrap.h"
#include "boxes/filters/edit_filter_box.h"
#include "boxes/premium_limits_box.h"
#include "core/application.h" // primaryWindow
#include "core/ui_integration.h"
#include "data/data_chat_filters.h"
#include "data/data_premium_limits.h"
#include "data/data_session.h"
#include "data/data_channel.h"
#include "data/data_user.h"
#include "history/history.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "ui/empty_userpic.h"
#include "ui/filter_icons.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "ui/text/text_utilities.h" // tr::bold
#include "ui/toast/toast.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/menu/menu_action.h"
#include "ui/widgets/popup_menu.h"
#include "window/window_controller.h"
#include "window/window_session_controller.h"
#include "main/main_session_settings.h"
#include "styles/style_dialogs.h"
#include "styles/style_media_player.h" // mediaPlayerMenuCheck
#include "styles/style_menu_icons.h"
#include "styles/style_settings.h"

namespace {

[[nodiscard]] QImage Icon(const Data::ChatFilter &f) {
	constexpr auto kScale = 0.75;
	const auto icon = Ui::LookupFilterIcon(Ui::ComputeFilterIcon(f)).normal;
	const auto originalWidth = icon->width();
	const auto originalHeight = icon->height();

	const auto scaledWidth = int(originalWidth * kScale);
	const auto scaledHeight = int(originalHeight * kScale);

	auto image = QImage(
		scaledWidth * style::DevicePixelRatio(),
		scaledHeight * style::DevicePixelRatio(),
		QImage::Format_ARGB32_Premultiplied);
	image.setDevicePixelRatio(style::DevicePixelRatio());
	image.fill(Qt::transparent);

	{
		auto p = QPainter(&image);
		auto hq = PainterHighQualityEnabler(p);

		const auto x = int((scaledWidth - originalWidth * kScale) / 2);
		const auto y = int((scaledHeight - originalHeight * kScale) / 2);

		p.scale(kScale, kScale);
		icon->paint(p, x, y, scaledWidth, st::dialogsUnreadBgMuted->c);
		if (const auto color = f.colorIndex()) {
			p.resetTransform();
			const auto circleSize = scaledWidth / 3.;
			const auto r = QRectF(
				x + scaledWidth - circleSize,
				y + scaledHeight - circleSize - circleSize / 3.,
				circleSize,
				circleSize);
			p.setPen(Qt::NoPen);
			p.setCompositionMode(QPainter::CompositionMode_Clear);
			p.setBrush(Qt::transparent);
			p.drawEllipse(r + Margins(st::lineWidth * 1.5));
			p.setCompositionMode(QPainter::CompositionMode_SourceOver);
			p.setBrush(Ui::EmptyUserpic::UserpicColor(*color).color2);
			p.drawEllipse(r);
		}
	}

	return image;
}

class FilterAction : public Ui::Menu::Action {
public:
	using Ui::Menu::Action::Action;

	void setIcon(QImage &&image) {
		_icon = std::move(image);
	}

protected:
	void paintEvent(QPaintEvent *event) override {
		Ui::Menu::Action::paintEvent(event);
		if (!_icon.isNull()) {
			const auto size = _icon.size() / style::DevicePixelRatio();
			auto p = QPainter(this);
			p.drawImage(
				width()
					- size.width()
					- st::menuWithIcons.itemPadding.right(),
				(height() - size.height()) / 2,
				_icon);
		}
	}

private:
	QImage _icon;

};

Data::ChatFilter ChangedFilter(
		const Data::ChatFilter &filter,
		not_null<History*> history,
		bool add) {
	auto always = base::duplicate(filter.always());
	auto never = base::duplicate(filter.never());
	if (add) {
		never.remove(history);
	} else {
		always.remove(history);
	}
	const auto result = Data::ChatFilter(
		filter.id(),
		filter.title(),
		filter.iconEmoji(),
		filter.colorIndex(),
		filter.flags(),
		std::move(always),
		filter.pinned(),
		std::move(never));
	const auto in = result.contains(history);
	if (in == add) {
		return result;
	}
	always = base::duplicate(result.always());
	never = base::duplicate(result.never());
	if (add) {
		always.insert(history);
	} else {
		never.insert(history);
	}
	return Data::ChatFilter(
		filter.id(),
		filter.title(),
		filter.iconEmoji(),
		filter.colorIndex(),
		filter.flags(),
		std::move(always),
		filter.pinned(),
		std::move(never));
}

void ChangeFilterById(
		FilterId filterId,
		not_null<History*> history,
		bool add) {
	Expects(filterId != 0);

	const auto list = history->owner().chatsFilters().list();
	const auto i = ranges::find(list, filterId, &Data::ChatFilter::id);
	if (i != end(list)) {
		const auto was = *i;
		const auto filter = ChangedFilter(was, history, add);
		history->owner().chatsFilters().set(filter);
		history->session().api().request(MTPmessages_UpdateDialogFilter(
			MTP_flags(MTPmessages_UpdateDialogFilter::Flag::f_filter),
			MTP_int(filter.id()),
			filter.tl()
		)).done([=, chat = history->peer->name(), name = filter.title()] {
			const auto account = not_null(&history->session().account());
			if (const auto controller = Core::App().windowFor(account)) {
				const auto isStatic = name.isStatic;
				controller->showToast({
					.text = (add
						? tr::lng_filters_toast_add
						: tr::lng_filters_toast_remove)(
							tr::now,
							lt_chat,
							tr::bold(chat),
							lt_folder,
							Ui::Text::Wrapped(name.text, EntityType::Bold),
							tr::marked),
					.textContext = Core::TextContext({
						.session = &history->session(),
						.customEmojiLoopLimit = isStatic ? -1 : 0,
					}),
				});
			}
		}).fail([=](const MTP::Error &error) {
			LOG(("API Error: failed to %1 a dialog to a folder. %2")
				.arg(add ? u"add"_q : u"remove"_q)
				.arg(error.type()));
			// Revert filter on fail.
			history->owner().chatsFilters().set(was);
		}).send();
	}
}

} // namespace

ChooseFilterValidator::ChooseFilterValidator(not_null<History*> history)
: _history(history) {
}

bool ChooseFilterValidator::canAdd() const {
	for (const auto &filter : _history->owner().chatsFilters().list()) {
		if (filter.id() && !filter.contains(_history)) {
			return true;
		}
	}
	return false;
}

bool ChooseFilterValidator::canAdd(FilterId filterId) const {
	Expects(filterId != 0);

	const auto list = _history->owner().chatsFilters().list();
	const auto i = ranges::find(list, filterId, &Data::ChatFilter::id);
	if (i != end(list)) {
		return !i->contains(_history);
	}
	return false;
}

bool ChooseFilterValidator::canRemove(FilterId filterId) const {
	Expects(filterId != 0);

	const auto list = _history->owner().chatsFilters().list();
	const auto i = ranges::find(list, filterId, &Data::ChatFilter::id);
	if (i != end(list)) {
		return Data::CanRemoveFromChatFilter(*i, _history);
	}
	return false;
}

ChooseFilterValidator::LimitData ChooseFilterValidator::limitReached(
		FilterId filterId,
		bool always) const {
	Expects(filterId != 0);

	const auto list = _history->owner().chatsFilters().list();
	const auto i = ranges::find(list, filterId, &Data::ChatFilter::id);
	const auto limit = _history->owner().pinnedChatsLimit(filterId);
	const auto &chatsList = always ? i->always() : i->never();
	return {
		.reached = (i != end(list))
			&& !ranges::contains(chatsList, _history)
			&& (chatsList.size() >= limit),
		.count = int(chatsList.size()),
	};
}

void ChooseFilterValidator::add(FilterId filterId) const {
	ChangeFilterById(filterId, _history, true);
}

void ChooseFilterValidator::remove(FilterId filterId) const {
	ChangeFilterById(filterId, _history, false);
}

void FillChooseFilterMenu(
		not_null<Window::SessionController*> controller,
		not_null<Ui::PopupMenu*> menu,
		not_null<History*> history) {
	const auto weak = base::make_weak(controller);
	const auto validator = ChooseFilterValidator(history);
	const auto &list = history->owner().chatsFilters().list();
	const auto showColors = history->owner().chatsFilters().tagsEnabled();
	for (const auto &filter : list) {
		const auto id = filter.id();
		if (!id) {
			continue;
		}

		auto callback = [=] {
			const auto toAdd = !filter.contains(history);
			const auto r = validator.limitReached(id, toAdd);
			if (r.reached) {
				controller->show(Box(
					FilterChatsLimitBox,
					&controller->session(),
					r.count,
					toAdd));
				return;
			} else if (toAdd ? validator.canAdd() : validator.canRemove(id)) {
				if (toAdd) {
					validator.add(id);
				} else {
					validator.remove(id);
				}
			}
		};

		const auto contains = filter.contains(history);
		const auto title = filter.title();
		auto item = base::make_unique_q<FilterAction>(
			menu->menu(),
			menu->st().menu,
			Ui::Menu::CreateAction(
				menu.get(),
				Ui::Text::FixAmpersandInAction(title.text.text),
				std::move(callback)),
			contains ? &st::mediaPlayerMenuCheck : nullptr,
			contains ? &st::mediaPlayerMenuCheck : nullptr);
		item->setMarkedText(title.text, QString(), Core::TextContext({
			.session = &history->session(),
			.repaint = [raw = item.get()] { raw->update(); },
			.customEmojiLoopLimit = title.isStatic ? -1 : 0,
		}));

		item->setIcon(Icon(showColors ? filter : filter.withColorIndex({})));
		const auto action = menu->addAction(std::move(item));
		action->setEnabled(contains
			? validator.canRemove(id)
			: validator.canAdd());
	}

	const auto limit = [session = &controller->session()] {
		return Data::PremiumLimits(session).dialogFiltersCurrent();
	};
	if ((list.size() - 1) < limit()) {
		menu->addAction(tr::lng_filters_create(tr::now), [=] {
			const auto strong = weak.get();
			if (!strong) {
				return;
			}
			const auto session = &strong->session();
			const auto &list = session->data().chatsFilters().list();
			if ((list.size() - 1) >= limit()) {
				return;
			}
			const auto chooseNextId = [&] {
				auto id = 2;
				while (ranges::contains(list, id, &Data::ChatFilter::id)) {
					++id;
				}
				return id;
			};
			auto filter =
				Data::ChatFilter({}, {}, {}, {}, {}, { history }, {}, {});
			const auto send = [=](const Data::ChatFilter &filter) {
				session->api().request(MTPmessages_UpdateDialogFilter(
					MTP_flags(MTPmessages_UpdateDialogFilter::Flag::f_filter),
					MTP_int(chooseNextId()),
					filter.tl()
				)).done([=] {
					session->data().chatsFilters().reload();
				}).send();
			};
			strong->uiShow()->show(
				Box(EditFilterBox, strong, std::move(filter), send, nullptr));
		}, &st::menuIconShowInFolder);
	}

	history->owner().chatsFilters().changed(
	) | rpl::on_next([=] {
		menu->hideMenu();
	}, menu->lifetime());
}

bool FillChooseFilterWithAdminedGroupsMenu(
		not_null<Window::SessionController*> controller,
		not_null<Ui::PopupMenu*> menu,
		not_null<UserData*> user,
		std::shared_ptr<rpl::event_stream<>> listUpdates,
		std::vector<not_null<PeerData*>> common,
		std::shared_ptr<std::vector<PeerId>> collectCommon) {
	const auto weak = base::make_weak(controller);
	const auto session = &controller->session();
	const auto &list = session->data().chatsFilters().list();
	const auto showColors = session->data().chatsFilters().tagsEnabled();
	auto added = 0;
	for (const auto &filter : list) {
		const auto id = filter.id();
		if (!id) {
			continue;
		}
		auto canRestrictList = std::vector<not_null<PeerData*>>();
		const auto maybeAppend = [&](not_null<History*> chat) {
			const auto channel = chat->peer->asChannel();
			if (channel && channel->canRestrictParticipant(user)) {
				if (channel->isGroupAdmin(user) && !channel->amCreator()) {
					return;
				}
				canRestrictList.push_back(chat->peer);
			}
		};
		for (const auto &chat : filter.always()) {
			maybeAppend(chat);
		}
		for (const auto &chat : filter.pinned()) {
			maybeAppend(chat);
		}
		if (canRestrictList.empty()) {
			continue;
		}

		const auto checked = std::make_shared<bool>(false);

		const auto contains = false;
		const auto title = filter.title();
		auto item = base::make_unique_q<FilterAction>(
			menu->menu(),
			menu->st().menu,
			new QAction(
				Ui::Text::FixAmpersandInAction(title.text.text),
				menu.get()),
			contains ? &st::mediaPlayerMenuCheck : nullptr,
			contains ? &st::mediaPlayerMenuCheck : nullptr);
		const auto triggered = [=, raw = item.get()] {
			*checked = !*checked;
			if (*checked) {
				for (const auto &peer : canRestrictList) {
					if (ranges::contains(common, peer)) {
						collectCommon->push_back(peer->id);
					}
				}
			} else {
				for (const auto &peer : canRestrictList) {
					if (const auto i = ranges::find(*collectCommon, peer->id);
							i != collectCommon->end()) {
						collectCommon->erase(i);
					}
				}
			}
			raw->Ui::Menu::Action::setIcon(
				*checked ? &st::mediaPlayerMenuCheck : nullptr,
				*checked ? &st::mediaPlayerMenuCheck : nullptr);
			listUpdates->fire({});
		};
		item->setActionTriggered([=] {
			triggered();

			auto groups = session->settings().moderateCommonGroups();
			if (*checked && !ranges::contains(groups, id)) {
				groups.push_back(id);
			} else if (!*checked) {
				groups.erase(ranges::remove(groups, id), groups.end());
			}
			session->settings().setModerateCommonGroups(groups);
			session->saveSettingsDelayed();
		});
		if (ranges::contains(
				session->settings().moderateCommonGroups(),
				id)) {
			triggered();
		}
		item->setPreventClose(true);
		item->setMarkedText(title.text, QString(), Core::TextContext({
			.session = session,
			.repaint = [raw = item.get()] { raw->update(); },
			.customEmojiLoopLimit = title.isStatic ? -1 : 0,
		}));

		item->setIcon(Icon(showColors ? filter : filter.withColorIndex({})));
		menu->addAction(std::move(item));
		added++;
	}

	session->data().chatsFilters().changed(
	) | rpl::on_next([=] {
		menu->hideMenu();
	}, menu->lifetime());

	return added;
}

History *HistoryFromMimeData(
		const QMimeData *mime,
		not_null<Main::Session*> session) {
	const auto mimeFormat = u"application/x-telegram-dialog"_q;
	if (mime->hasFormat(mimeFormat)) {
		auto peerId = int64(-1);
		auto isTestMode = false;
		auto stream = QDataStream(mime->data(mimeFormat));
		stream >> peerId;
		stream >> isTestMode;
		if (isTestMode != session->isTestMode()) {
			return nullptr;
		}
		return session->data().historyLoaded(PeerId(peerId));
	}
	if (mime->hasText()) {
		auto text = mime->text().trimmed();
		if (text.startsWith('@')) {
			text = text.mid(1);
		} else if (text.startsWith(u"https://t.me/"_q)) {
			text = text.mid(13);
		} else {
			return nullptr;
		}
		if (const auto peer = session->data().peerByUsername(text)) {
			return session->data().historyLoaded(peer->id);
		}
	}
	return nullptr;
}

void SetupFilterDragAndDrop(
		not_null<Ui::RpWidget*> outer,
		not_null<Main::Session*> session,
		Fn<std::optional<FilterId>(QPoint)> filterIdAtPosition,
		Fn<FilterId()> activeFilterId,
		Fn<void(FilterId)> selectByFilterId) {
	const auto hasAction = [=](not_null<QDropEvent*> drop, bool perform) {
		const auto mimeData = drop->mimeData();
		const auto filterId = filterIdAtPosition(
			outer->mapToGlobal(drop->pos()));
		if (!filterId) {
			return false;
		}
		const auto id = *filterId;
		if (const auto h = HistoryFromMimeData(mimeData, session)) {
			auto v = ChooseFilterValidator(h);
			if (id) {
				if (v.canAdd(id)) {
					if (!v.limitReached(id, true).reached) {
						if (perform) {
							v.add(id);
						}
						selectByFilterId(perform ? FilterId(-1) : id);
						return true;
					}
				}
			} else {
				if (const auto active = activeFilterId();
						active && v.canRemove(active)) {
					if (perform) {
						v.remove(active);
					}
					selectByFilterId(perform ? FilterId(-1) : active);
					return true;
				}
			}
		}
		selectByFilterId(-1);
		return false;
	};
	outer->setAcceptDrops(true);
	outer->events(
	) | rpl::filter([](not_null<QEvent*> e) {
		return e->type() == QEvent::DragEnter
			|| e->type() == QEvent::DragMove
			|| e->type() == QEvent::DragLeave
			|| e->type() == QEvent::Drop;
	}) | rpl::on_next([=](not_null<QEvent*> e) {
		if (e->type() == QEvent::DragEnter) {
			const auto de = static_cast<QDragEnterEvent*>(e.get());
			if (hasAction(de, false)) {
				de->acceptProposedAction();
			} else {
				de->ignore();
			}
		} else if (e->type() == QEvent::DragMove) {
			const auto dm = static_cast<QDragMoveEvent*>(e.get());
			if (hasAction(dm, false)) {
				dm->acceptProposedAction();
			} else {
				dm->ignore();
			}
		} else if (e->type() == QEvent::DragLeave) {
			selectByFilterId(-1);
		} else if (e->type() == QEvent::Drop) {
			const auto drop = static_cast<QDropEvent*>(e.get());
			if (hasAction(drop, true)) {
				drop->acceptProposedAction();
			} else {
				drop->ignore();
			}
		}
	}, outer->lifetime());
}
