/*
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 "payments/ui/payments_field.h"

#include "ui/widgets/fields/input_field.h"
#include "ui/boxes/country_select_box.h"
#include "ui/text/format_values.h"
#include "ui/ui_utility.h"
#include "ui/widgets/fields/special_fields.h"
#include "countries/countries_instance.h"
#include "base/platform/base_platform_info.h"
#include "base/event_filter.h"
#include "base/qt/qt_common_adapters.h"
#include "styles/style_payments.h"

#include <QtCore/QRegularExpression>
#include <QtWidgets/QTextEdit>

namespace Payments::Ui {
namespace {

struct SimpleFieldState {
	QString value;
	int position = 0;
};

[[nodiscard]] char FieldThousandsSeparator(const CurrencyRule &rule) {
	return (rule.thousands == '.' || rule.thousands == ',')
		? ' '
		: rule.thousands;
}

[[nodiscard]] QString RemoveNonNumbers(QString value) {
	static const auto RegExp = QRegularExpression("[^0-9]");
	return value.replace(RegExp, QString());
}

[[nodiscard]] SimpleFieldState CleanMoneyState(
		const CurrencyRule &rule,
		SimpleFieldState state) {
	const auto withDecimal = state.value.replace(
		QChar('.'),
		rule.decimal
	).replace(
		QChar(','),
		rule.decimal
	);
	const auto digitsLimit = 16 - rule.exponent;
	const auto beforePosition = state.value.mid(0, state.position);
	auto decimalPosition = int(withDecimal.lastIndexOf(rule.decimal));
	if (decimalPosition < 0) {
		state = {
			.value = RemoveNonNumbers(state.value),
			.position = int(RemoveNonNumbers(beforePosition).size()),
		};
	} else {
		const auto onlyNumbersBeforeDecimal = RemoveNonNumbers(
			state.value.mid(0, decimalPosition));
		state = {
			.value = (onlyNumbersBeforeDecimal
				+ QChar(rule.decimal)
				+ RemoveNonNumbers(state.value.mid(decimalPosition + 1))),
			.position = int(RemoveNonNumbers(beforePosition).size()
				+ (state.position > decimalPosition ? 1 : 0)),
		};
		decimalPosition = onlyNumbersBeforeDecimal.size();
		const auto maxLength = decimalPosition + 1 + rule.exponent;
		if (state.value.size() > maxLength) {
			state = {
				.value = state.value.mid(0, maxLength),
				.position = std::min(state.position, maxLength),
			};
		}
	}
	if (!state.value.isEmpty() && state.value[0] == QChar(rule.decimal)) {
		state = {
			.value = QChar('0') + state.value,
			.position = state.position + 1,
		};
		if (decimalPosition >= 0) {
			++decimalPosition;
		}
	}
	auto skip = 0;
	while (state.value.size() > skip + 1
		&& state.value[skip] == QChar('0')
		&& state.value[skip + 1] != QChar(rule.decimal)) {
		++skip;
	}
	state = {
		.value = state.value.mid(skip),
		.position = std::max(state.position - skip, 0),
	};
	if (decimalPosition >= 0) {
		Assert(decimalPosition >= skip);
		decimalPosition -= skip;
		if (decimalPosition > digitsLimit) {
			state = {
				.value = (state.value.mid(0, digitsLimit)
					+ state.value.mid(decimalPosition)),
				.position = (state.position > digitsLimit
					? std::max(
						state.position - (decimalPosition - digitsLimit),
						digitsLimit)
					: state.position),
			};
		}
	} else if (state.value.size() > digitsLimit) {
		state = {
			.value = state.value.mid(0, digitsLimit),
			.position = std::min(state.position, digitsLimit),
		};
	}
	return state;
}

[[nodiscard]] SimpleFieldState PostprocessMoneyResult(
		const CurrencyRule &rule,
		SimpleFieldState result) {
	const auto position = result.value.indexOf(rule.decimal);
	const auto from = (position >= 0) ? position : result.value.size();
	for (auto insertAt = from - 3; insertAt > 0; insertAt -= 3) {
		result.value.insert(insertAt, QChar(FieldThousandsSeparator(rule)));
		if (result.position >= insertAt) {
			++result.position;
		}
	}
	return result;
}

[[nodiscard]] bool IsBackspace(const FieldValidateRequest &request) {
	return (request.wasAnchor == request.wasPosition)
		&& (request.wasPosition == request.nowPosition + 1)
		&& (base::StringViewMid(request.wasValue, 0, request.wasPosition - 1)
			== base::StringViewMid(request.nowValue, 0, request.nowPosition))
		&& (base::StringViewMid(request.wasValue, request.wasPosition)
			== base::StringViewMid(request.nowValue, request.nowPosition));
}

[[nodiscard]] bool IsDelete(const FieldValidateRequest &request) {
	return (request.wasAnchor == request.wasPosition)
		&& (request.wasPosition == request.nowPosition)
		&& (base::StringViewMid(request.wasValue, 0, request.wasPosition)
			== base::StringViewMid(request.nowValue, 0, request.nowPosition))
		&& (base::StringViewMid(request.wasValue, request.wasPosition + 1)
			== base::StringViewMid(request.nowValue, request.nowPosition));
}

[[nodiscard]] auto MoneyValidator(const CurrencyRule &rule) {
	return [=](FieldValidateRequest request) {
		const auto realNowState = [&] {
			const auto backspaced = IsBackspace(request);
			const auto deleted = IsDelete(request);
			if (!backspaced && !deleted) {
				return CleanMoneyState(rule, {
					.value = request.nowValue,
					.position = request.nowPosition,
				});
			}
			const auto realWasState = CleanMoneyState(rule, {
				.value = request.wasValue,
				.position = request.wasPosition,
			});
			const auto changedValue = deleted
				? (realWasState.value.mid(0, realWasState.position)
					+ realWasState.value.mid(realWasState.position + 1))
				: (realWasState.position > 1)
				? (realWasState.value.mid(0, realWasState.position - 1)
					+ realWasState.value.mid(realWasState.position))
				: realWasState.value.mid(realWasState.position);
			return SimpleFieldState{
				.value = changedValue,
				.position = (deleted
					? realWasState.position
					: std::max(realWasState.position - 1, 0))
			};
		}();
		const auto postprocessed = PostprocessMoneyResult(
			rule,
			realNowState);
		return FieldValidateResult{
			.value = postprocessed.value,
			.position = postprocessed.position,
		};
	};
}

[[nodiscard]] QString Parse(const FieldConfig &config) {
	if (config.type == FieldType::Country) {
		return Countries::Instance().countryNameByISO2(config.value);
	} else if (config.type == FieldType::Money) {
		const auto amount = config.value.toLongLong();
		if (!amount) {
			return QString();
		}
		const auto rule = LookupCurrencyRule(config.currency);
		const auto value = std::abs(amount) / std::pow(10., rule.exponent);
		const auto precision = (!rule.stripDotZero
			|| std::floor(value) != value)
			? rule.exponent
			: 0;
		return FormatWithSeparators(
			value,
			precision,
			rule.decimal,
			FieldThousandsSeparator(rule));
	}
	return config.value;
}

[[nodiscard]] QString Format(
		const FieldConfig &config,
		const QString &parsed,
		const QString &countryIso2) {
	if (config.type == FieldType::Country) {
		return countryIso2;
	} else if (config.type == FieldType::Money) {
		static const auto RegExp = QRegularExpression("[^0-9\\.]");
		const auto rule = LookupCurrencyRule(config.currency);
		const auto real = QString(parsed).replace(
			QChar(rule.decimal),
			QChar('.')
		).replace(
			QChar(','),
			QChar('.')
		).replace(
			RegExp,
			QString()
		).toDouble();
		return QString::number(
			int64(base::SafeRound(real * std::pow(10., rule.exponent))));
	} else if (config.type == FieldType::CardNumber
		|| config.type == FieldType::CardCVC) {
		static const auto RegExp = QRegularExpression("[^0-9]");
		return QString(parsed).replace(RegExp, QString());
	}
	return parsed;
}

[[nodiscard]] bool UseMaskedField(FieldType type) {
	switch (type) {
	case FieldType::Text:
	case FieldType::Email:
		return false;
	case FieldType::CardNumber:
	case FieldType::CardExpireDate:
	case FieldType::CardCVC:
	case FieldType::Country:
	case FieldType::Phone:
	case FieldType::Money:
		return true;
	}
	Unexpected("FieldType in Payments::Ui::UseMaskedField.");
}

[[nodiscard]] base::unique_qptr<RpWidget> CreateWrap(
		QWidget *parent,
		FieldConfig &config) {
	switch (config.type) {
	case FieldType::Text:
	case FieldType::Email:
		return base::make_unique_q<InputField>(
			parent,
			st::paymentsField,
			std::move(config.placeholder),
			Parse(config));
	case FieldType::CardNumber:
	case FieldType::CardExpireDate:
	case FieldType::CardCVC:
	case FieldType::Country:
	case FieldType::Phone:
	case FieldType::Money:
		return base::make_unique_q<RpWidget>(parent);
	}
	Unexpected("FieldType in Payments::Ui::CreateWrap.");
}

[[nodiscard]] InputField *LookupInputField(
		not_null<RpWidget*> wrap,
		FieldConfig &config) {
	return UseMaskedField(config.type)
		? nullptr
		: static_cast<InputField*>(wrap.get());
}

[[nodiscard]] MaskedInputField *CreateMoneyField(
		not_null<RpWidget*> wrap,
		FieldConfig &config,
		rpl::producer<> textPossiblyChanged) {
	struct State {
		CurrencyRule rule;
		style::InputField st;
		QString currencyText;
		int currencySkip = 0;
		FlatLabel *left = nullptr;
		FlatLabel *right = nullptr;
	};
	const auto state = wrap->lifetime().make_state<State>(State{
		.rule = LookupCurrencyRule(config.currency),
		.st = st::paymentsMoneyField,
	});
	const auto &rule = state->rule;
	state->currencySkip = rule.space ? state->st.style.font->spacew : 0;
	state->currencyText = ((!rule.left && rule.space)
		? QString(QChar(' '))
		: QString()) + (*rule.international
			? QString(rule.international)
			: config.currency) + ((rule.left && rule.space)
				? QString(QChar(' '))
				: QString());
	if (rule.left) {
		state->left = CreateChild<FlatLabel>(
			wrap.get(),
			state->currencyText,
			st::paymentsFieldAdditional);
	}
	state->right = CreateChild<FlatLabel>(
		wrap.get(),
		QString(),
		st::paymentsFieldAdditional);
	const auto leftSkip = state->left
		? (state->left->textMaxWidth() + state->currencySkip)
		: 0;
	const auto rightSkip = st::paymentsFieldAdditional.style.font->width(
		QString(QChar(rule.decimal))
		+ QString(QChar('0')).repeated(rule.exponent)
		+ (rule.left ? QString() : state->currencyText));
	state->st.textMargins += QMargins(leftSkip, 0, rightSkip, 0);
	state->st.placeholderMargins -= QMargins(leftSkip, 0, rightSkip, 0);
	const auto result = CreateChild<MaskedInputField>(
		wrap.get(),
		state->st,
		std::move(config.placeholder),
		Parse(config));
	result->setPlaceholderHidden(true);
	if (state->left) {
		state->left->move(0, state->st.textMargins.top());
	}
	const auto updateRight = [=] {
		const auto text = result->getLastText();
		const auto width = state->st.style.font->width(text);
		const auto &rule = state->rule;
		const auto symbol = QChar(rule.decimal);
		const auto decimal = text.indexOf(symbol);
		const auto zeros = (decimal >= 0)
			? std::max(rule.exponent - int(text.size() - decimal - 1), 0)
			: rule.stripDotZero
			? 0
			: rule.exponent;
		const auto valueDecimalSeparator = (decimal >= 0 || !zeros)
			? QString()
			: QString(symbol);
		const auto zeroString = QString(QChar('0'));
		const auto valueRightPart = (text.isEmpty() ? zeroString : QString())
			+ valueDecimalSeparator
			+ zeroString.repeated(zeros);
		const auto right = valueRightPart
			+ (rule.left ? QString() : state->currencyText);
		state->right->setText(right);
		state->right->setTextColorOverride(valueRightPart.isEmpty()
			? std::nullopt
			: std::make_optional(st::windowSubTextFg->c));
		state->right->move(
			(state->st.textMargins.left()
				+ width
				+ ((rule.left || !valueRightPart.isEmpty())
					? 0
					: state->currencySkip)),
			state->st.textMargins.top());
	};
	std::move(
		textPossiblyChanged
	) | rpl::on_next(updateRight, result->lifetime());
	if (state->left) {
		state->left->raise();
	}
	state->right->raise();
	return result;
}

[[nodiscard]] MaskedInputField *LookupMaskedField(
		not_null<RpWidget*> wrap,
		FieldConfig &config,
		rpl::producer<> textPossiblyChanged) {
	if (!UseMaskedField(config.type)) {
		return nullptr;
	}
	switch (config.type) {
	case FieldType::Text:
	case FieldType::Email:
		return nullptr;
	case FieldType::CardNumber:
	case FieldType::CardExpireDate:
	case FieldType::CardCVC:
	case FieldType::Country:
		return CreateChild<MaskedInputField>(
			wrap.get(),
			st::paymentsField,
			std::move(config.placeholder),
			Parse(config));
	case FieldType::Phone:
		return CreateChild<PhoneInput>(
			wrap.get(),
			st::paymentsField,
			std::move(config.placeholder),
			Countries::ExtractPhoneCode(config.defaultPhone),
			Parse(config),
			[](const QString &s) { return Countries::Groups(s); });
	case FieldType::Money:
		return CreateMoneyField(
			wrap,
			config,
			std::move(textPossiblyChanged));
	}
	Unexpected("FieldType in Payments::Ui::LookupMaskedField.");
}

} // namespace

Field::Field(QWidget *parent, FieldConfig &&config)
: _config(config)
, _wrap(CreateWrap(parent, config))
, _input(LookupInputField(_wrap.get(), config))
, _masked(LookupMaskedField(
	_wrap.get(),
	config,
	_textPossiblyChanged.events_starting_with({})))
, _countryIso2(config.value) {
	if (_masked) {
		setupMaskedGeometry();
	}
	if (_config.type == FieldType::Country) {
		setupCountry();
	}
	if (const auto &validator = config.validator) {
		setupValidator(validator);
	} else if (config.type == FieldType::Money) {
		setupValidator(MoneyValidator(LookupCurrencyRule(config.currency)));
	}
	setupFrontBackspace();
	setupSubmit();
}

RpWidget *Field::widget() const {
	return _wrap.get();
}

object_ptr<RpWidget> Field::ownedWidget() const {
	return object_ptr<RpWidget>::fromRaw(_wrap.get());
}

QString Field::value() const {
	return Format(
		_config,
		_input ? _input->getLastText() : _masked->getLastText(),
		_countryIso2);
}

rpl::producer<> Field::frontBackspace() const {
	return _frontBackspace.events();
}

rpl::producer<> Field::finished() const {
	return _finished.events();
}

rpl::producer<> Field::submitted() const {
	return _submitted.events();
}

void Field::setupMaskedGeometry() {
	Expects(_masked != nullptr);

	_wrap->resize(_masked->size());
	_wrap->widthValue(
	) | rpl::on_next([=](int width) {
		_masked->resize(width, _masked->height());
	}, _masked->lifetime());
	_masked->heightValue(
	) | rpl::on_next([=](int height) {
		_wrap->resize(_wrap->width(), height);
	}, _masked->lifetime());
}

void Field::setupCountry() {
	Expects(_config.type == FieldType::Country);
	Expects(_masked != nullptr);

	QObject::connect(_masked, &MaskedInputField::focused, [=] {
		setFocus();

		const auto name = Countries::Instance().countryNameByISO2(
			_countryIso2);
		const auto country = !name.isEmpty()
			? _countryIso2
			: !_config.defaultCountry.isEmpty()
			? _config.defaultCountry
			: Platform::SystemCountry();
		auto box = Box<CountrySelectBox>(
			country,
			CountrySelectBox::Type::Countries);
		const auto raw = box.data();
		raw->countryChosen(
		) | rpl::on_next([=](QString iso2) {
			_countryIso2 = iso2;
			_masked->setText(Countries::Instance().countryNameByISO2(iso2));
			_masked->hideError();
			raw->closeBox();
			if (!iso2.isEmpty()) {
				if (_nextField) {
					_nextField->activate();
				} else {
					_submitted.fire({});
				}
			}
		}, _masked->lifetime());
		raw->boxClosing() | rpl::on_next([=] {
			setFocus();
		}, _masked->lifetime());
		_config.showBox(std::move(box));
	});
}

void Field::setupValidator(Fn<ValidateResult(ValidateRequest)> validator) {
	Expects(validator != nullptr);

	const auto state = [=]() -> State {
		if (_masked) {
			const auto position = _masked->cursorPosition();
			const auto selectionStart = _masked->selectionStart();
			const auto selectionEnd = _masked->selectionEnd();
			return {
				.value = _masked->getLastText(),
				.position = position,
				.anchor = (selectionStart == selectionEnd
					? position
					: (selectionStart == position)
					? selectionEnd
					: selectionStart),
			};
		}
		const auto cursor = _input->textCursor();
		return {
			.value = _input->getLastText(),
			.position = cursor.position(),
			.anchor = cursor.anchor(),
		};
	};
	const auto save = [=] {
		_was = state();
	};
	const auto setText = [=](const QString &text) {
		if (_masked) {
			_masked->setText(text);
		} else {
			_input->setText(text);
		}
	};
	const auto setPosition = [=](int position) {
		if (_masked) {
			_masked->setCursorPosition(position);
		} else {
			auto cursor = _input->textCursor();
			cursor.setPosition(position);
			_input->setTextCursor(cursor);
		}
	};
	const auto validate = [=] {
		if (_validating) {
			return;
		}
		_validating = true;
		const auto guard = gsl::finally([&] {
			_validating = false;
			save();
			_textPossiblyChanged.fire({});
		});

		const auto now = state();
		const auto result = validator(ValidateRequest{
			.wasValue = _was.value,
			.wasPosition = _was.position,
			.wasAnchor = _was.anchor,
			.nowValue = now.value,
			.nowPosition = now.position,
		});
		_valid = result.finished || !result.invalid;

		const auto changed = (result.value != now.value);
		if (changed) {
			setText(result.value);
		}
		if (changed || result.position != now.position) {
			setPosition(result.position);
		}
		if (result.finished) {
			_finished.fire({});
		} else if (result.invalid) {
			Ui::PostponeCall(
				_masked ? (QWidget*)_masked : _input,
				[=] { showErrorNoFocus(); });
		}
	};
	if (_masked) {
		QObject::connect(_masked, &QLineEdit::cursorPositionChanged, save);
		QObject::connect(_masked, &MaskedInputField::changed, validate);
	} else {
		const auto raw = _input->rawTextEdit();
		QObject::connect(raw, &QTextEdit::cursorPositionChanged, save);
		_input->changes(
		) | rpl::on_next(validate, _input->lifetime());
	}
}

void Field::setupFrontBackspace() {
	const auto filter = [=](not_null<QEvent*> e) {
		const auto frontBackspace = (e->type() == QEvent::KeyPress)
			&& (static_cast<QKeyEvent*>(e.get())->key() == Qt::Key_Backspace)
			&& (_masked
				? (_masked->cursorPosition() == 0
					&& _masked->selectionLength() == 0)
				: (_input->textCursor().position() == 0
					&& _input->textCursor().anchor() == 0));
		if (frontBackspace) {
			_frontBackspace.fire({});
		}
		return base::EventFilterResult::Continue;
	};
	if (_masked) {
		base::install_event_filter(_masked, filter);
	} else {
		base::install_event_filter(_input->rawTextEdit(), filter);
	}
}

void Field::setupSubmit() {
	const auto submitted = [=] {
		if (!_valid) {
			showError();
		} else if (_nextField) {
			_nextField->activate();
		} else {
			_submitted.fire({});
		}
	};
	if (_masked) {
		QObject::connect(_masked, &MaskedInputField::submitted, submitted);
	} else {
		_input->submits(
		) | rpl::on_next(submitted, _input->lifetime());
	}
}

void Field::setNextField(not_null<Field*> field) {
	_nextField = field;

	finished() | rpl::on_next([=] {
		field->setFocus();
	}, _masked ? _masked->lifetime() : _input->lifetime());
}

void Field::setPreviousField(not_null<Field*> field) {
	frontBackspace(
	) | rpl::on_next([=] {
		field->setFocus();
	}, _masked ? _masked->lifetime() : _input->lifetime());
}

void Field::activate() {
	if (_input) {
		_input->setFocus();
	} else {
		_masked->setFocus();
	}
}

void Field::setFocus() {
	if (_config.type == FieldType::Country) {
		_wrap->setFocus();
	} else {
		activate();
	}
}

void Field::setFocusFast() {
	if (_config.type == FieldType::Country) {
		setFocus();
	} else if (_input) {
		_input->setFocusFast();
	} else {
		_masked->setFocusFast();
	}
}

void Field::showError() {
	if (_config.type == FieldType::Country) {
		setFocus();
		_masked->showErrorNoFocus();
	} else if (_input) {
		_input->showError();
	} else {
		_masked->showError();
	}
}

void Field::showErrorNoFocus() {
	if (_input) {
		_input->showErrorNoFocus();
	} else {
		_masked->showErrorNoFocus();
	}
}

} // namespace Payments::Ui
