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

#include "ui/widgets/buttons.h"
#include "ui/effects/radial_animation.h"
#include "ui/painter.h"
#include "ui/ui_utility.h"
#include "mtproto/mtp_instance.h"
#include "mtproto/facade.h"
#include "main/main_account.h"
#include "core/application.h"
#include "core/core_settings.h"
#include "core/update_checker.h"
#include "boxes/connection_box.h"
#include "boxes/abstract_box.h"
#include "lang/lang_keys.h"
#include "styles/style_window.h"

#include <QtGui/QWindow>

namespace Window {
namespace {

constexpr auto kIgnoreStartConnectingFor = crl::time(3000);
constexpr auto kConnectingStateDelay = crl::time(1000);
constexpr auto kRefreshTimeout = crl::time(200);
constexpr auto kMinimalWaitingStateDuration = crl::time(4000);

class Progress : public Ui::RpWidget {
public:
	Progress(QWidget *parent);

	rpl::producer<> animationStepRequests() const;

protected:
	void paintEvent(QPaintEvent *e) override;

private:
	void animationStep();

	Ui::InfiniteRadialAnimation _animation;
	rpl::event_stream<> _animationStepRequests;

};

Progress::Progress(QWidget *parent)
: RpWidget(parent)
, _animation([=] { animationStep(); }, st::connectingRadial) {
	setAttribute(Qt::WA_OpaquePaintEvent);
	setAttribute(Qt::WA_TransparentForMouseEvents);
	resize(st::connectingRadial.size);
	_animation.start(st::connectingRadial.sineDuration);
}

void Progress::paintEvent(QPaintEvent *e) {
	auto p = QPainter(this);

	p.fillRect(e->rect(), st::windowBg);
	const auto &st = st::connectingRadial;
	const auto shift = st.thickness - (st.thickness / 2);
	_animation.draw(
		p,
		{ shift, shift },
		QSize(st.size.width() - 2 * shift, st.size.height() - 2 * shift),
		width());
}

void Progress::animationStep() {
	if (!anim::Disabled()) {
		_animationStepRequests.fire({});
		update();
	}
}

rpl::producer<> Progress::animationStepRequests() const {
	return _animationStepRequests.events();
}

} // namespace

class ConnectionState::Widget : public Ui::AbstractButton {
public:
	Widget(
		QWidget *parent,
		not_null<Main::Account*> account,
		const Layout &layout);

	void refreshRetryLink(bool hasRetry);
	void setLayout(const Layout &layout);
	void setProgressVisibility(bool visible);

	rpl::producer<> refreshStateRequests() const;

protected:
	void resizeEvent(QResizeEvent *e) override;
	void paintEvent(QPaintEvent *e) override;

	void onStateChanged(State was, StateChangeSource source) override;

private:
	class ProxyIcon;
	using State = ConnectionState::State;
	using Layout = ConnectionState::Layout;

	void updateRetryGeometry();
	QRect innerRect() const;
	QRect contentRect() const;
	QRect textRect() const;

	const not_null<Main::Account*> _account;
	Layout _currentLayout;
	base::unique_qptr<Ui::LinkButton> _retry;
	QPointer<Progress> _progress;
	QPointer<ProxyIcon> _proxyIcon;
	rpl::event_stream<> _refreshStateRequests;

};

class ConnectionState::Widget::ProxyIcon final : public Ui::RpWidget {
public:
	ProxyIcon(QWidget *parent);

	void setToggled(bool toggled);
	void setOpacity(float64 opacity);

protected:
	void paintEvent(QPaintEvent *e) override;

private:
	void refreshCacheImages();

	float64 _opacity = 1.;
	QPixmap _cacheOn;
	QPixmap _cacheOff;
	bool _toggled = true;

};

ConnectionState::Widget::ProxyIcon::ProxyIcon(QWidget *parent) : RpWidget(parent) {
	resize(
		std::max(
			st::connectingRadial.size.width(),
			st::connectingProxyOn.width()),
		std::max(
			st::connectingRadial.size.height(),
			st::connectingProxyOn.height()));

	style::PaletteChanged(
	) | rpl::start_with_next([=] {
		refreshCacheImages();
	}, lifetime());

	refreshCacheImages();
}

void ConnectionState::Widget::ProxyIcon::refreshCacheImages() {
	const auto prepareCache = [&](const style::icon &icon) {
		auto image = QImage(
			size() * style::DevicePixelRatio(),
			QImage::Format_ARGB32_Premultiplied);
		image.setDevicePixelRatio(style::DevicePixelRatio());
		image.fill(st::windowBg->c);
		{
			auto p = QPainter(&image);
			icon.paint(
				p,
				(width() - icon.width()) / 2,
				(height() - icon.height()) / 2,
				width());
		}
		return Ui::PixmapFromImage(std::move(image));
	};
	_cacheOn = prepareCache(st::connectingProxyOn);
	_cacheOff = prepareCache(st::connectingProxyOff);
}

void ConnectionState::Widget::ProxyIcon::setToggled(bool toggled) {
	if (_toggled != toggled) {
		_toggled = toggled;
		update();
	}
}

void ConnectionState::Widget::ProxyIcon::setOpacity(float64 opacity) {
	_opacity = opacity;
	if (_opacity == 0.) {
		hide();
	} else if (isHidden()) {
		show();
	}
	update();
}

void ConnectionState::Widget::ProxyIcon::paintEvent(QPaintEvent *e) {
	auto p = QPainter(this);
	p.setOpacity(_opacity);
	p.drawPixmap(0, 0, _toggled ? _cacheOn : _cacheOff);
}

bool ConnectionState::State::operator==(const State &other) const {
	return (type == other.type)
		&& (useProxy == other.useProxy)
		&& (underCursor == other.underCursor)
		&& (updateReady == other.updateReady)
		&& (waitTillRetry == other.waitTillRetry);
}

ConnectionState::ConnectionState(
	not_null<Ui::RpWidget*> parent,
	not_null<Main::Account*> account,
	rpl::producer<bool> shown)
: _account(account)
, _parent(parent)
, _refreshTimer([=] { refreshState(); })
, _currentLayout(computeLayout(_state)) {
	rpl::combine(
		std::move(shown),
		visibility()
	) | rpl::start_with_next([=](bool shown, float64 visible) {
		if (!shown || visible == 0.) {
			_widget = nullptr;
		} else if (!_widget) {
			createWidget();
		}
	}, _lifetime);

	if (!Core::UpdaterDisabled()) {
		Core::UpdateChecker checker;
		rpl::merge(
			rpl::single(rpl::empty),
			checker.ready()
		) | rpl::start_with_next([=] {
			refreshState();
		}, _lifetime);
	}

	rpl::combine(
		Core::App().settings().proxy().connectionTypeValue(),
		rpl::single(QRect()) | rpl::then(_parent->paintRequest())
	) | rpl::start_with_next([=] {
		refreshState();
	}, _lifetime);
}

void ConnectionState::createWidget() {
	_widget = base::make_unique_q<Widget>(_parent, _account, _currentLayout);
	_widget->setVisible(!_forceHidden);

	updateWidth();

	rpl::combine(
		visibility(),
		_parent->heightValue(),
		_bottomSkip.value()
	) | rpl::start_with_next([=](float64 visible, int height, int skip) {
		_widget->moveToLeft(0, anim::interpolate(
			height - st::connectingMargin.top(),
			height - _widget->height() - skip,
			visible));
	}, _widget->lifetime());

	_widget->refreshStateRequests(
	) | rpl::start_with_next([=] {
		refreshState();
	}, _widget->lifetime());
}

void ConnectionState::raise() {
	if (_widget) {
		_widget->raise();
	}
}

void ConnectionState::finishAnimating() {
	if (_contentWidth.animating()) {
		_contentWidth.stop();
		updateWidth();
	}
	if (_visibility.animating()) {
		_visibility.stop();
		updateVisibility();
	}
}

void ConnectionState::setForceHidden(bool hidden) {
	_forceHidden = hidden;
	if (_widget) {
		_widget->setVisible(!hidden);
	}
}

void ConnectionState::setBottomSkip(int skip) {
	_bottomSkip = skip;
}

void ConnectionState::refreshState() {
	using Checker = Core::UpdateChecker;
	const auto state = [&]() -> State {
		const auto exposed = _parent->window()->windowHandle()
			&& _parent->window()->windowHandle()->isExposed();
		const auto under = _widget && _widget->isOver();
		const auto ready = (Checker().state() == Checker::State::Ready);
		const auto state = _account->mtp().dcstate();
		const auto proxy = Core::App().settings().proxy().isEnabled();
		if (state == MTP::ConnectingState
			|| state == MTP::DisconnectedState
			|| (state < 0 && state > -600)) {
			return { State::Type::Connecting, proxy, exposed, under, ready };
		} else if (state < 0
			&& state >= -kMinimalWaitingStateDuration
			&& _state.type != State::Type::Waiting) {
			return { State::Type::Connecting, proxy, exposed, under, ready };
		} else if (state < 0) {
			const auto wait = ((-state) / 1000) + 1;
			return { State::Type::Waiting, proxy, exposed, under, ready, wait };
		}
		return { State::Type::Connected, proxy, exposed, under, ready };
	}();
	if (state.exposed && state.waitTillRetry > 0) {
		_refreshTimer.callOnce(kRefreshTimeout);
	}
	if (state == _state) {
		return;
	} else if (state.type == State::Type::Connecting
		&& _state.type == State::Type::Connected) {
		const auto now = crl::now();
		if (!_connectingStartedAt) {
			_connectingStartedAt = now;
			_refreshTimer.callOnce(kConnectingStateDelay);
			return;
		}
		const auto applyConnectingAt = std::max(
			_connectingStartedAt + kConnectingStateDelay,
			kIgnoreStartConnectingFor);
		if (now < applyConnectingAt) {
			_refreshTimer.callOnce(applyConnectingAt - now);
			return;
		}
	}
	applyState(state);
}

void ConnectionState::applyState(const State &state) {
	const auto newLayout = computeLayout(state);
	const auto guard = gsl::finally([&] { updateWidth(); });

	_state = state;
	if (_currentLayout.visible != newLayout.visible) {
		changeVisibilityWithLayout(newLayout);
		return;
	}
	if (_currentLayout.contentWidth != newLayout.contentWidth) {
		if (!_currentLayout.contentWidth
			|| !newLayout.contentWidth
			|| _contentWidth.animating()) {
			_contentWidth.start(
				[=] { updateWidth(); },
				_currentLayout.contentWidth,
				newLayout.contentWidth,
				st::connectingDuration);
		}
	}
	const auto saved = _currentLayout;
	setLayout(newLayout);
	if (_currentLayout.text.isEmpty()
		&& !saved.text.isEmpty()
		&& _contentWidth.animating()) {
		_currentLayout.text = saved.text;
		_currentLayout.textWidth = saved.textWidth;
	}
}

void ConnectionState::changeVisibilityWithLayout(const Layout &layout) {
	Expects(_currentLayout.visible != layout.visible);

	const auto changeLayout = !_currentLayout.visible;
	_visibility.start(
		[=] { updateVisibility(); },
		layout.visible ? 0. : 1.,
		layout.visible ? 1. : 0.,
		st::connectingDuration);
	if (_contentWidth.animating()) {
		_contentWidth.start(
			[=] { updateWidth(); },
			_currentLayout.contentWidth,
			(changeLayout ? layout : _currentLayout).contentWidth,
			st::connectingDuration);
	}
	if (changeLayout) {
		setLayout(layout);
	} else {
		_currentLayout.visible = layout.visible;
	}
}

void ConnectionState::setLayout(const Layout &layout) {
	_currentLayout = layout;
	if (_widget) {
		_widget->setLayout(layout);
	}
	refreshProgressVisibility();
}

void ConnectionState::refreshProgressVisibility() {
	if (_widget) {
		_widget->setProgressVisibility(_contentWidth.animating()
			|| _currentLayout.progressShown);
	}
}

void ConnectionState::updateVisibility() {
	const auto value = currentVisibility();
	if (value == 0. && _contentWidth.animating()) {
		_contentWidth.stop();
		updateWidth();
	}
	_visibilityValues.fire_copy(value);
}

float64 ConnectionState::currentVisibility() const {
	return _visibility.value(_currentLayout.visible ? 1. : 0.);
}

rpl::producer<float64> ConnectionState::visibility() const {
	return _visibilityValues.events_starting_with(currentVisibility());
}

auto ConnectionState::computeLayout(const State &state) const -> Layout {
	auto result = Layout();
	result.proxyEnabled = state.useProxy;
	result.progressShown = (state.type != State::Type::Connected);
	result.visible = state.exposed
		&& !state.updateReady
		&& (state.useProxy
			|| state.type == State::Type::Connecting
			|| state.type == State::Type::Waiting);
	switch (state.type) {
	case State::Type::Connecting:
		result.text = state.underCursor
			? tr::lng_connecting(tr::now)
			: QString();
		break;

	case State::Type::Waiting:
		Assert(state.waitTillRetry > 0);
		result.text = tr::lng_reconnecting(
			tr::now,
			lt_count,
			state.waitTillRetry);
		break;
	}
	result.textWidth = st::normalFont->width(result.text);
	result.contentWidth = (result.textWidth > 0)
		? (st::connectingTextPadding.left()
			+ result.textWidth
			+ st::connectingTextPadding.right())
		: 0;
	if (state.type == State::Type::Waiting) {
		result.contentWidth += st::connectingRetryLink.padding.left()
			+ st::connectingRetryLink.font->width(
				tr::lng_reconnecting_try_now(tr::now))
			+ st::connectingRetryLink.padding.right();
	}
	result.hasRetry = (state.type == State::Type::Waiting);
	return result;
}

void ConnectionState::updateWidth() {
	const auto current = _contentWidth.value(_currentLayout.contentWidth);
	const auto height = st::connectingLeft.height();
	const auto desired = QRect(0, 0, current, height).marginsAdded(
		style::margins(
			st::connectingLeft.width(),
			0,
			st::connectingRight.width(),
			0)
	).marginsAdded(
		st::connectingMargin
	);
	if (_widget) {
		_widget->resize(desired.size());
		_widget->update();
	}
	refreshProgressVisibility();
}

ConnectionState::Widget::Widget(
	QWidget *parent,
	not_null<Main::Account*> account,
	const Layout &layout)
: AbstractButton(parent)
, _account(account)
, _currentLayout(layout) {
	_proxyIcon = Ui::CreateChild<ProxyIcon>(this);
	_progress = Ui::CreateChild<Progress>(this);

	addClickHandler([=] {
		Ui::show(ProxiesBoxController::CreateOwningBox(account));
	});

	_progress->animationStepRequests(
	) | rpl::start_with_next([=] {
		_refreshStateRequests.fire({});
	}, _progress->lifetime());
}

void ConnectionState::Widget::onStateChanged(
		AbstractButton::State was,
		StateChangeSource source) {
	Ui::PostponeCall(crl::guard(this, [=] {
		_refreshStateRequests.fire({});
	}));
}

rpl::producer<> ConnectionState::Widget::refreshStateRequests() const {
	return _refreshStateRequests.events();
}

void ConnectionState::Widget::paintEvent(QPaintEvent *e) {
	Painter p(this);
	PainterHighQualityEnabler hq(p);

	p.setPen(Qt::NoPen);
	p.setBrush(st::windowBg);
	const auto inner = innerRect();
	const auto content = contentRect();
	const auto text = textRect();
	const auto left = inner.topLeft();
	const auto right = content.topLeft() + QPoint(content.width(), 0);
	st::connectingLeftShadow.paint(p, left, width());
	st::connectingLeft.paint(p, left, width());
	st::connectingRightShadow.paint(p, right, width());
	st::connectingRight.paint(p, right, width());
	st::connectingBodyShadow.fill(p, content);
	st::connectingBody.fill(p, content);

	const auto available = text.width();
	if (available > 0 && !_currentLayout.text.isEmpty()) {
		p.setFont(st::normalFont);
		p.setPen(st::windowSubTextFg);
		if (available >= _currentLayout.textWidth) {
			p.drawTextLeft(
				text.x(),
				text.y(),
				width(),
				_currentLayout.text,
				_currentLayout.textWidth);
		} else {
			p.drawTextLeft(
				text.x(),
				text.y(),
				width(),
				st::normalFont->elided(_currentLayout.text, available));
		}
	}
}

QRect ConnectionState::Widget::innerRect() const {
	return rect().marginsRemoved(
		st::connectingMargin
	);
}

QRect ConnectionState::Widget::contentRect() const {
	return innerRect().marginsRemoved(style::margins(
		st::connectingLeft.width(),
		0,
		st::connectingRight.width(),
		0));
}

QRect ConnectionState::Widget::textRect() const {
	return contentRect().marginsRemoved(
		st::connectingTextPadding
	);
}

void ConnectionState::Widget::resizeEvent(QResizeEvent *e) {
	{
		const auto xShift = (height() - _progress->width()) / 2;
		const auto yShift = (height() - _progress->height()) / 2;
		_progress->moveToLeft(xShift, yShift);
	}
	{
		const auto xShift = (height() - _proxyIcon->width()) / 2;
		const auto yShift = (height() - _proxyIcon->height()) / 2;
		_proxyIcon->moveToRight(xShift, yShift);
	}
	updateRetryGeometry();
}

void ConnectionState::Widget::updateRetryGeometry() {
	if (!_retry) {
		return;
	}
	const auto text = textRect();
	const auto available = text.width() - _currentLayout.textWidth;
	if (available <= 0) {
		_retry->hide();
	} else {
		_retry->show();
		_retry->resize(
			std::min(available, _retry->naturalWidth()),
			innerRect().height());
		_retry->moveToLeft(
			text.x() + text.width() - _retry->width(),
			st::connectingMargin.top());
	}
}

void ConnectionState::Widget::setLayout(const Layout &layout) {
	_currentLayout = layout;
	_proxyIcon->setToggled(_currentLayout.proxyEnabled);
	refreshRetryLink(_currentLayout.hasRetry);
}

void ConnectionState::Widget::setProgressVisibility(bool visible) {
	if (_progress->isHidden() == visible) {
		_progress->setVisible(visible);
	}
}

void ConnectionState::Widget::refreshRetryLink(bool hasRetry) {
	if (hasRetry && !_retry) {
		_retry = base::make_unique_q<Ui::LinkButton>(
			this,
			tr::lng_reconnecting_try_now(tr::now),
			st::connectingRetryLink);
		_retry->addClickHandler([=] {
			_account->mtp().restart();
		});
		updateRetryGeometry();
	} else if (!hasRetry) {
		_retry = nullptr;
	}
}

} // namespace Window
