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

#include "lang/lang_keys.h"
#include "data/data_poll.h"
#include "ui/toast/toast.h"
#include "ui/wrap/vertical_layout.h"
#include "ui/wrap/slide_wrap.h"
#include "ui/widgets/input_fields.h"
#include "ui/widgets/shadow.h"
#include "ui/widgets/labels.h"
#include "ui/widgets/buttons.h"
#include "core/event_filter.h"
#include "chat_helpers/emoji_suggestions_widget.h"
#include "settings/settings_common.h"
#include "base/unique_qptr.h"
#include "styles/style_boxes.h"
#include "styles/style_settings.h"

namespace {

constexpr auto kQuestionLimit = 255;
constexpr auto kMaxOptionsCount = PollData::kMaxOptions;
constexpr auto kOptionLimit = 100;
constexpr auto kWarnQuestionLimit = 80;
constexpr auto kWarnOptionLimit = 30;
constexpr auto kErrorLimit = 99;

class Options {
public:
	Options(
		not_null<QWidget*> outer,
		not_null<Ui::VerticalLayout*> container);

	[[nodiscard]] bool isValid() const;
	[[nodiscard]] rpl::producer<bool> isValidChanged() const;
	[[nodiscard]] std::vector<PollAnswer> toPollAnswers() const;
	void focusFirst();

	[[nodiscard]] rpl::producer<int> usedCount() const;
	[[nodiscard]] rpl::producer<not_null<QWidget*>> scrollToWidget() const;
	[[nodiscard]] rpl::producer<> backspaceInFront() const;

private:
	class Option {
	public:
		static Option Create(
			not_null<QWidget*> outer,
			not_null<Ui::VerticalLayout*> container,
			int position);

		void toggleRemoveAlways(bool toggled);

		void show(anim::type animated);
		void destroy(FnMut<void()> done);

		//[[nodisacrd]] bool hasShadow() const;
		//void destroyShadow();

		[[nodiscard]] bool isEmpty() const;
		[[nodiscard]] bool isGood() const;
		[[nodiscard]] bool isTooLong() const;
		[[nodiscard]] bool hasFocus() const;
		void setFocus() const;
		void clearValue();

		void setPlaceholder() const;
		void removePlaceholder() const;

		not_null<Ui::InputField*> field() const;

		[[nodiscard]] PollAnswer toPollAnswer(int index) const;

		[[nodiscard]] rpl::producer<Qt::MouseButton> removeClicks() const;

		inline bool operator<(const Option &other) const {
			return field() < other.field();
		}

		friend inline bool operator<(
				const Option &option,
				Ui::InputField *field) {
			return option.field() < field;
		}
		friend inline bool operator<(
				Ui::InputField *field,
				const Option &option) {
			return field < option.field();
		}

	private:
		Option() = default;

		void createShadow();
		void createRemove();
		void createWarning();

		base::unique_qptr<Ui::SlideWrap<Ui::InputField>> _field;
		base::unique_qptr<Ui::PlainShadow> _shadow;
		base::unique_qptr<Ui::CrossButton> _remove;
		rpl::variable<bool> *_removeAlways = nullptr;

	};

	[[nodiscard]] bool full() const;
	//[[nodiscard]] bool correctShadows() const;
	//void fixShadows();
	void removeEmptyTail();
	void addEmptyOption();
	void checkLastOption();
	void validateState();
	void fixAfterErase();
	void destroy(Option &&option);
	void removeDestroyed(not_null<Ui::InputField*> field);
	int findField(not_null<Ui::InputField*> field) const;

	not_null<QWidget*> _outer;
	not_null<Ui::VerticalLayout*> _container;
	int _position = 0;
	std::vector<Option> _list;
	std::set<Option, std::less<>> _destroyed;
	rpl::variable<bool> _valid = false;
	rpl::variable<int> _usedCount = 0;
	rpl::event_stream<not_null<QWidget*>> _scrollToWidget;
	rpl::event_stream<> _backspaceInFront;

};

void InitField(
		not_null<QWidget*> container,
		not_null<Ui::InputField*> field) {
	field->setInstantReplaces(Ui::InstantReplaces::Default());
	field->setInstantReplacesEnabled(Global::ReplaceEmojiValue());
	auto options = Ui::Emoji::SuggestionsController::Options();
	options.suggestExactFirstWord = false;
	Ui::Emoji::SuggestionsController::Init(container, field, options);
}

not_null<Ui::FlatLabel*> CreateWarningLabel(
		not_null<QWidget*> parent,
		not_null<Ui::InputField*> field,
		int valueLimit,
		int warnLimit) {
	const auto result = Ui::CreateChild<Ui::FlatLabel>(
		parent.get(),
		QString(),
		Ui::FlatLabel::InitType::Simple,
		st::createPollWarning);
	result->setAttribute(Qt::WA_TransparentForMouseEvents);
	QObject::connect(field, &Ui::InputField::changed, [=] {
		Ui::PostponeCall(crl::guard(field, [=] {
			const auto length = field->getLastText().size();
			const auto value = valueLimit - length;
			const auto shown = (value < warnLimit)
				&& (field->height() > st::createPollOptionField.heightMin);
			result->setRichText((value >= 0)
				? QString::number(value)
				: textcmdLink(1, QString::number(value)));
			result->setVisible(shown);
		}));
	});
	return result;
}

void FocusAtEnd(not_null<Ui::InputField*> field) {
	field->setFocus();
	field->setCursorPosition(field->getLastText().size());
	field->ensureCursorVisible();
}

Options::Option Options::Option::Create(
		not_null<QWidget*> outer,
		not_null<Ui::VerticalLayout*> container,
		int position) {
	auto result = Option();
	const auto field = container->insert(
		position,
		object_ptr<Ui::SlideWrap<Ui::InputField>>(
			container,
			object_ptr<Ui::InputField>(
				container,
				st::createPollOptionField,
				Ui::InputField::Mode::NoNewlines,
				langFactory(lng_polls_create_option_add))));
	InitField(outer, field->entity());
	field->entity()->setMaxLength(kOptionLimit + kErrorLimit);
	result._field.reset(field);

	result.createShadow();
	result.createRemove();
	result.createWarning();
	return result;
}

//bool Options::Option::hasShadow() const {
//	return (_shadow != nullptr);
//}

void Options::Option::createShadow() {
	Expects(_field != nullptr);

	if (_shadow) {
		return;
	}
	const auto value = Ui::CreateChild<Ui::PlainShadow>(field().get());
	value->show();
	field()->sizeValue(
	) | rpl::start_with_next([=](QSize size) {
		const auto left = st::createPollFieldPadding.left();
		value->setGeometry(
			left,
			size.height() - st::lineWidth,
			size.width() - left,
			st::lineWidth);
	}, value->lifetime());
	_shadow.reset(value);
}

//void Options::Option::destroyShadow() {
//	_shadow = nullptr;
//}

void Options::Option::createRemove() {
	using namespace rpl::mappers;

	const auto field = this->field();
	auto &lifetime = field->lifetime();

	const auto remove = Ui::CreateChild<Ui::CrossButton>(
		field.get(),
		st::createPollOptionRemove);
	remove->hide(anim::type::instant);

	const auto toggle = lifetime.make_state<rpl::variable<bool>>(false);
	_removeAlways = lifetime.make_state<rpl::variable<bool>>(false);

	QObject::connect(field, &Ui::InputField::changed, [=] {
		// Don't capture 'this'! Because Option is a value type.
		*toggle = !field->getLastText().isEmpty();
	});
	rpl::combine(
		toggle->value(),
		_removeAlways->value(),
		_1 || _2
	) | rpl::start_with_next([=](bool shown) {
		remove->toggle(shown, anim::type::normal);
	}, remove->lifetime());

	field->widthValue(
	) | rpl::start_with_next([=](int width) {
		remove->moveToRight(
			st::createPollOptionRemovePosition.x(),
			st::createPollOptionRemovePosition.y(),
			width);
	}, remove->lifetime());

	_remove.reset(remove);
}

void Options::Option::createWarning() {
	using namespace rpl::mappers;

	const auto field = this->field();
	const auto warning = CreateWarningLabel(
		field,
		field,
		kOptionLimit,
		kWarnOptionLimit);
	rpl::combine(
		field->sizeValue(),
		warning->sizeValue()
	) | rpl::start_with_next([=](QSize size, QSize label) {
		warning->moveToLeft(
			(size.width()
				- label.width()
				- st::createPollWarningPosition.x()),
			(size.height()
				- label.height()
				- st::createPollWarningPosition.y()),
			size.width());
	}, warning->lifetime());
}

bool Options::Option::isEmpty() const {
	return field()->getLastText().trimmed().isEmpty();
}

bool Options::Option::isGood() const {
	return !field()->getLastText().trimmed().isEmpty() && !isTooLong();
}

bool Options::Option::isTooLong() const {
	return (field()->getLastText().size() > kOptionLimit);
}

bool Options::Option::hasFocus() const {
	return field()->hasFocus();
}

void Options::Option::setFocus() const {
	FocusAtEnd(field());
}

void Options::Option::clearValue() {
	field()->setText(QString());
}

void Options::Option::setPlaceholder() const {
	field()->setPlaceholder(langFactory(lng_polls_create_option_add));
}

void Options::Option::toggleRemoveAlways(bool toggled) {
	*_removeAlways = toggled;
}

not_null<Ui::InputField*> Options::Option::field() const {
	return _field->entity();
}

void Options::Option::removePlaceholder() const {
	field()->setPlaceholder(nullptr);
}

PollAnswer Options::Option::toPollAnswer(int index) const {
	Expects(index >= 0 && index < kMaxOptionsCount);

	return PollAnswer{
		field()->getLastText().trimmed(),
		QByteArray(1, ('0' + index))
	};
}

rpl::producer<Qt::MouseButton> Options::Option::removeClicks() const {
	return _remove->clicks();
}

Options::Options(
	not_null<QWidget*> outer,
	not_null<Ui::VerticalLayout*> container)
: _outer(outer)
, _container(container)
, _position(_container->count()) {
	checkLastOption();
}

bool Options::full() const {
	return (_list.size() == kMaxOptionsCount);
}

bool Options::isValid() const {
	return _valid.current();
}

rpl::producer<bool> Options::isValidChanged() const {
	return _valid.changes();
}

rpl::producer<int> Options::usedCount() const {
	return _usedCount.value();
}

rpl::producer<not_null<QWidget*>> Options::scrollToWidget() const {
	return _scrollToWidget.events();
}

rpl::producer<> Options::backspaceInFront() const {
	return _backspaceInFront.events();
}

void Options::Option::show(anim::type animated) {
	_field->hide(anim::type::instant);
	_field->show(animated);
}

void Options::Option::destroy(FnMut<void()> done) {
	if (anim::Disabled() || _field->isHidden()) {
		Ui::PostponeCall(std::move(done));
		return;
	}
	_field->hide(anim::type::normal);
	App::CallDelayed(
		st::slideWrapDuration * 2,
		_field.get(),
		std::move(done));
}

std::vector<PollAnswer> Options::toPollAnswers() const {
	auto result = std::vector<PollAnswer>();
	result.reserve(_list.size());
	auto counter = int(0);
	const auto makeAnswer = [&](const Option &option) {
		return option.toPollAnswer(counter++);
	};
	ranges::copy(
		_list
		| ranges::view::filter(&Option::isGood)
		| ranges::view::transform(makeAnswer),
		ranges::back_inserter(result));
	return result;
}

void Options::focusFirst() {
	Expects(!_list.empty());

	_list.front().setFocus();
}
//
//bool Options::correctShadows() const {
//	// Last one should be without shadow if all options were used.
//	const auto noShadow = ranges::find(
//		_list,
//		true,
//		ranges::not_fn(&Option::hasShadow));
//	return (noShadow == end(_list) - (full() ? 1 : 0));
//}
//
//void Options::fixShadows() {
//	if (correctShadows()) {
//		return;
//	}
//	for (auto &option : _list) {
//		option.createShadow();
//	}
//	if (full()) {
//		_list.back().destroyShadow();
//	}
//}

void Options::removeEmptyTail() {
	// Only one option at the end of options list can be empty.
	// Remove all other trailing empty options.
	// Only last empty and previous option have non-empty placeholders.
	const auto focused = ranges::find_if(
		_list,
		&Option::hasFocus);
	const auto end = _list.end();
	const auto reversed = ranges::view::reverse(_list);
	const auto emptyItem = ranges::find_if(
		reversed,
		ranges::not_fn(&Option::isEmpty)).base();
	const auto focusLast = (focused > emptyItem) && (focused < end);
	if (emptyItem == end) {
		return;
	}
	if (focusLast) {
		emptyItem->setFocus();
	}
	for (auto i = emptyItem + 1; i != end; ++i) {
		destroy(std::move(*i));
	}
	_list.erase(emptyItem + 1, end);
	fixAfterErase();
}

void Options::destroy(Option &&option) {
	const auto field = option.field();
	option.destroy([=] { removeDestroyed(field); });
	_destroyed.emplace(std::move(option));
}

void Options::fixAfterErase() {
	Expects(!_list.empty());

	const auto last = _list.end() - 1;
	last->setPlaceholder();
	last->toggleRemoveAlways(false);
	if (last != begin(_list)) {
		(last - 1)->setPlaceholder();
		(last - 1)->toggleRemoveAlways(false);
	}
}

void Options::addEmptyOption() {
	if (full()) {
		return;
	} else if (!_list.empty() && _list.back().isEmpty()) {
		return;
	}
	if (_list.size() > 1) {
		(_list.end() - 2)->removePlaceholder();
		(_list.end() - 2)->toggleRemoveAlways(true);
	}
	_list.push_back(Option::Create(
		_outer,
		_container,
		_position + _list.size() + _destroyed.size()));
	const auto field = _list.back().field();
	QObject::connect(field, &Ui::InputField::submitted, [=] {
		const auto index = findField(field);
		if (_list[index].isGood() && index + 1 < _list.size()) {
			_list[index + 1].setFocus();
		}
	});
	QObject::connect(field, &Ui::InputField::changed, [=] {
		Ui::PostponeCall(crl::guard(field, [=] {
			validateState();
		}));
	});
	QObject::connect(field, &Ui::InputField::focused, [=] {
		_scrollToWidget.fire_copy(field);
	});
	Core::InstallEventFilter(field, [=](not_null<QEvent*> event) {
		if (event->type() != QEvent::KeyPress
			|| !field->getLastText().isEmpty()) {
			return false;
		}
		const auto key = static_cast<QKeyEvent*>(event.get())->key();
		if (key != Qt::Key_Backspace) {
			return false;
		}

		const auto index = findField(field);
		if (index > 0) {
			_list[index - 1].setFocus();
		} else {
			_backspaceInFront.fire({});
		}
		return true;
	});

	_list.back().removeClicks(
	) | rpl::start_with_next([=] {
		Ui::PostponeCall(crl::guard(field, [=] {
			Expects(!_list.empty());

			const auto item = begin(_list) + findField(field);
			if (item == _list.end() - 1) {
				item->clearValue();
				return;
			}
			if (item->hasFocus()) {
				(item + 1)->setFocus();
			}
			destroy(std::move(*item));
			_list.erase(item);
			fixAfterErase();
			validateState();
		}));
	}, field->lifetime());

	_list.back().show((_list.size() == 1)
		? anim::type::instant
		: anim::type::normal);
	//fixShadows();
}

void Options::removeDestroyed(not_null<Ui::InputField*> field) {
	_destroyed.erase(_destroyed.find(field));
}

void Options::validateState() {
	checkLastOption();
	_valid = (ranges::count_if(_list, &Option::isGood) > 1)
		&& (ranges::find_if(_list, &Option::isTooLong) == end(_list));
	const auto lastEmpty = !_list.empty() && _list.back().isEmpty();
	_usedCount = _list.size() - (lastEmpty ? 1 : 0);
}

int Options::findField(not_null<Ui::InputField*> field) const {
	const auto result = ranges::find(
		_list,
		field,
		&Option::field) - begin(_list);

	Ensures(result >= 0 && result < _list.size());
	return result;
}

void Options::checkLastOption() {
	removeEmptyTail();
	addEmptyOption();
}

} // namespace

CreatePollBox::CreatePollBox(QWidget*) {
}

rpl::producer<PollData> CreatePollBox::submitRequests() const {
	return _submitRequests.events();
}

void CreatePollBox::setInnerFocus() {
	_setInnerFocus();
}

void CreatePollBox::submitFailed(const QString &error) {
	Ui::Toast::Show(error);
}

not_null<Ui::InputField*> CreatePollBox::setupQuestion(
		not_null<Ui::VerticalLayout*> container) {
	using namespace Settings;

	AddSubsectionTitle(container, lng_polls_create_question);
	const auto question = container->add(
		object_ptr<Ui::InputField>(
			container,
			st::createPollField,
			Ui::InputField::Mode::MultiLine,
			langFactory(lng_polls_create_question_placeholder)),
		st::createPollFieldPadding);
	InitField(getDelegate()->outerContainer(), question);
	question->setMaxLength(kQuestionLimit + kErrorLimit);

	const auto warning = CreateWarningLabel(
		container,
		question,
		kQuestionLimit,
		kWarnQuestionLimit);
	rpl::combine(
		question->geometryValue(),
		warning->sizeValue()
	) | rpl::start_with_next([=](QRect geometry, QSize label) {
		warning->moveToLeft(
			(container->width()
				- label.width()
				- st::createPollWarningPosition.x()),
			(geometry.y()
				- st::createPollFieldPadding.top()
				- st::settingsSubsectionTitlePadding.bottom()
				- st::settingsSubsectionTitle.style.font->height
				+ st::settingsSubsectionTitle.style.font->ascent
				- st::createPollWarning.style.font->ascent),
			geometry.width());
	}, warning->lifetime());

	return question;
}

object_ptr<Ui::RpWidget> CreatePollBox::setupContent() {
	using namespace Settings;

	const auto id = rand_value<uint64>();
	const auto valid = lifetime().make_state<rpl::event_stream<bool>>();

	auto result = object_ptr<Ui::VerticalLayout>(this);
	const auto container = result.data();

	const auto question = setupQuestion(container);
	AddDivider(container);
	AddSkip(container);
	AddSubsectionTitle(container, lng_polls_create_options);
	const auto options = lifetime().make_state<Options>(
		getDelegate()->outerContainer(),
		container);
	auto limit = options->usedCount() | rpl::after_next([=](int count) {
		setCloseByEscape(!count);
		setCloseByOutsideClick(!count);
	}) | rpl::map([=](int count) {
		return (count < kMaxOptionsCount)
			? lng_polls_create_limit(lt_count, kMaxOptionsCount - count)
			: lang(lng_polls_create_maximum);
	}) | rpl::after_next([=] {
		container->resizeToWidth(container->widthNoMargins());
	});
	container->add(
		object_ptr<Ui::FlatLabel>(
			container,
			std::move(limit),
			st::createPollLimitLabel),
		st::createPollLimitPadding);

	const auto isValidQuestion = [=] {
		const auto text = question->getLastText().trimmed();
		return !text.isEmpty() && (text.size() <= kQuestionLimit);
	};

	connect(question, &Ui::InputField::submitted, [=] {
		if (isValidQuestion()) {
			options->focusFirst();
		}
	});

	_setInnerFocus = [=] {
		question->setFocusFast();
	};

	const auto collectResult = [=] {
		auto result = PollData(id);
		result.question = question->getLastText().trimmed();
		result.answers = options->toPollAnswers();
		return result;
	};
	const auto updateValid = [=] {
		valid->fire(isValidQuestion() && options->isValid());
	};
	connect(question, &Ui::InputField::changed, [=] {
		updateValid();
	});
	valid->events_starting_with(
		false
	) | rpl::distinct_until_changed(
	) | rpl::start_with_next([=](bool valid) {
		clearButtons();
		if (valid) {
			addButton(
				langFactory(lng_polls_create_button),
				[=] { _submitRequests.fire(collectResult()); });
		}
		addButton(langFactory(lng_cancel), [=] { closeBox(); });
	}, lifetime());

	options->isValidChanged(
	) | rpl::start_with_next([=] {
		updateValid();
	}, lifetime());

	options->scrollToWidget(
	) | rpl::start_with_next([=](not_null<QWidget*> widget) {
		scrollToWidget(widget);
	}, lifetime());

	options->backspaceInFront(
	) | rpl::start_with_next([=] {
		FocusAtEnd(question);
	}, lifetime());

	return std::move(result);
}

void CreatePollBox::prepare() {
	setTitle(langFactory(lng_polls_create_title));

	const auto inner = setInnerWidget(setupContent());

	setDimensionsToContent(st::boxWideWidth, inner);
}
