/*
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 "platform/mac/touchbar/items/mac_pinned_chats_item.h"

#include "apiwrap.h"
#include "base/call_delayed.h"
#include "base/timer.h"
#include "base/unixtime.h"
#include "core/application.h"
#include "core/sandbox.h"
#include "data/data_changes.h"
#include "data/data_cloud_file.h"
#include "data/data_file_origin.h"
#include "data/data_folder.h"
#include "data/data_peer_values.h"
#include "data/data_session.h"
#include "data/data_user.h"
#include "history/history.h"
#include "main/main_session.h"
#include "mainwidget.h"
#include "platform/mac/touchbar/mac_touchbar_common.h"
#include "styles/style_dialogs.h"
#include "ui/effects/animations.h"
#include "ui/empty_userpic.h"
#include "ui/userpic_view.h"
#include "ui/unread_badge_paint.h"
#include "ui/painter.h"
#include "window/window_controller.h"
#include "window/window_session_controller.h"

#import <AppKit/NSColor.h>
#import <AppKit/NSGraphicsContext.h>
#import <AppKit/NSPressGestureRecognizer.h>

using TouchBar::kCircleDiameter;

namespace {

constexpr auto kPinnedButtonsSpace = 30;
constexpr auto kPinnedButtonsLeftSkip = kPinnedButtonsSpace / 2;

constexpr auto kOnlineCircleSize = 8;
constexpr auto kOnlineCircleStrokeWidth = 1.5;
constexpr auto kUnreadBadgeSize = 15;

inline bool IsSelfPeer(PeerData *peer) {
	return peer && peer->isSelf();
}

inline bool IsRepliesPeer(PeerData *peer) {
	return peer && peer->isRepliesChat();
}

QImage PrepareImage() {
	const auto s = kCircleDiameter * style::DevicePixelRatio();
	auto result = QImage(QSize(s, s), QImage::Format_ARGB32_Premultiplied);
	result.fill(Qt::transparent);
	return result;
}

QImage SavedMessagesUserpic() {
	auto result = PrepareImage();
	Painter paint(&result);

	const auto s = result.width();
	Ui::EmptyUserpic::PaintSavedMessages(paint, 0, 0, s, s);
	return result;
}

QImage RepliesMessagesUserpic() {
	auto result = PrepareImage();
	Painter paint(&result);

	const auto s = result.width();
	Ui::EmptyUserpic::PaintRepliesMessages(paint, 0, 0, s, s);
	return result;
}

QImage ArchiveUserpic(not_null<Data::Folder*> folder) {
	auto result = PrepareImage();
	Painter paint(&result);

	folder->paintUserpic(paint, 0, 0, result.width());
	return result;
}

QImage UnreadBadge(not_null<PeerData*> peer) {
	const auto history = peer->owner().history(peer->id);
	const auto state = history->chatListBadgesState();
	if (!state.unread) {
		return QImage();
	}
	const auto counter = (state.unreadCounter > 0)
		? QString::number(state.unreadCounter)
		: QString();
	Ui::UnreadBadgeStyle unreadSt;
	unreadSt.sizeId = Ui::UnreadBadgeSize::TouchBar;
	unreadSt.muted = state.unreadMuted;
	// Use constant values to draw badge regardless of cConfigScale().
	unreadSt.size = kUnreadBadgeSize * float64(style::DevicePixelRatio());
	unreadSt.padding = 4 * float64(style::DevicePixelRatio());
	unreadSt.font = style::font(
		9.5 * float64(style::DevicePixelRatio()),
		unreadSt.font->flags(),
		unreadSt.font->family());

	auto result = QImage(
		QSize(kCircleDiameter, kUnreadBadgeSize) * style::DevicePixelRatio(),
		QImage::Format_ARGB32_Premultiplied);
	result.fill(Qt::transparent);
	Painter p(&result);

	Ui::PaintUnreadBadge(
		p,
		counter,
		result.width(),
		result.height() - unreadSt.size,
		unreadSt,
		2);
	return result;
}

NSRect PeerRectByIndex(int index) {
	return NSMakeRect(
		index * (kCircleDiameter + kPinnedButtonsSpace)
			+ kPinnedButtonsLeftSkip,
		0,
		kCircleDiameter,
		kCircleDiameter);
}

[[nodiscard]] Data::LastseenStatus CalculateLastseenStatus(
		not_null<PeerData*> peer) {
	if (const auto user = peer->asUser()) {
		return user->lastseen();
	}
	return Data::LastseenStatus();
}

} // namespace

#pragma mark - PinnedDialogsPanel

@interface PinnedDialogsPanel()
@end // @interface PinnedDialogsPanel

@implementation PinnedDialogsPanel {
	struct Pin {
		PeerData *peer = nullptr;
		Ui::PeerUserpicView userpicView;
		int index = -1;
		QImage userpic;
		QImage unreadBadge;

		Ui::Animations::Simple shiftAnimation;
		int shift = 0;
		int finalShift = 0;
		int deltaShift = 0;
		int x = 0;
		int horizontalShift = 0;
		bool onTop = false;

		Ui::Animations::Simple onlineAnimation;
		Data::LastseenStatus lastseen;
	};
	rpl::lifetime _lifetime;
	Main::Session *_session;

	std::vector<std::unique_ptr<Pin>> _pins;
	QImage _savedMessages;
	QImage _repliesMessages;
	QImage _archive;

	bool _hasArchive;
	bool _selfUnpinned;
	bool _repliesUnpinned;

	rpl::event_stream<not_null<NSEvent*>> _touches;
	rpl::event_stream<not_null<NSPressGestureRecognizer*>> _gestures;

	CGFloat _r, _g, _b, _a; // The online circle color.
}

- (void)processHorizontalReorder {
	// This method is a simplified version of the VerticalLayoutReorder class
	// and is adapatized for horizontal use.
	enum class State : uchar {
		Started,
		Applied,
		Cancelled,
	};

	const auto currentStart = _lifetime.make_state<int>(0);
	const auto currentPeer = _lifetime.make_state<PeerData*>(nullptr);
	const auto currentState = _lifetime.make_state<State>(State::Cancelled);
	const auto currentDesiredIndex = _lifetime.make_state<int>(-1);
	const auto waitForFinish = _lifetime.make_state<bool>(false);
	const auto isDragging = _lifetime.make_state<bool>(false);

	const auto indexOf = [=](PeerData *p) {
		const auto i = ranges::find(_pins, p, &Pin::peer);
		Assert(i != end(_pins));
		return i - begin(_pins);
	};

	const auto setHorizontalShift = [=](const auto &pin, int shift) {
		if (const auto delta = shift - pin->horizontalShift) {
			pin->horizontalShift = shift;
			pin->x += delta;

			// Redraw a rectangle
			// from the beginning point of the pin movement to the end point.
			auto rect = PeerRectByIndex(indexOf(pin->peer) + [self shift]);
			const auto absDelta = std::abs(delta);
			rect.origin.x = pin->x - absDelta;
			rect.size.width += absDelta * 2;
			[self setNeedsDisplayInRect:rect];
		}
	};

	const auto updateShift = [=](not_null<PeerData*> peer, int indexHint) {
		Expects(indexHint >= 0 && indexHint < _pins.size());

		const auto index = (_pins[indexHint]->peer->id == peer->id)
			? indexHint
			: indexOf(peer);
		const auto &entry = _pins[index];
		entry->shift = entry->deltaShift
			+ base::SafeRound(
				entry->shiftAnimation.value(entry->finalShift));
		if (entry->deltaShift && !entry->shiftAnimation.animating()) {
			entry->finalShift += entry->deltaShift;
			entry->deltaShift = 0;
		}
		setHorizontalShift(entry, entry->shift);
	};

	const auto moveToShift = [=](int index, int shift) {
		Core::Sandbox::Instance().customEnterFromEventLoop([=] {
			auto &entry = _pins[index];
			if (entry->finalShift + entry->deltaShift == shift) {
				return;
			}
			const auto peer = entry->peer;
			entry->shiftAnimation.start(
				[=] { updateShift(peer, index); },
				entry->finalShift,
				shift - entry->deltaShift,
				st::slideWrapDuration);
			entry->finalShift = shift - entry->deltaShift;
		});
	};

	const auto cancelCurrentPeer = [=] {
		Expects(*currentPeer != nullptr);

		if (*currentState == State::Started) {
			*currentState = State::Cancelled;
		}
		*currentPeer = nullptr;
		for (auto i = 0, count = int(_pins.size()); i != count; ++i) {
			moveToShift(i, 0);
		}
	};

	const auto cancelCurrent = [=] {
		if (*currentPeer) {
			cancelCurrentPeer();
		}
	};

	const auto updateOrder = [=](int index, int positionX) {
		const auto shift = positionX - *currentStart;
		const auto &current = _pins[index];
		current->shiftAnimation.stop();
		current->shift = current->finalShift = shift;
		setHorizontalShift(current, shift);

		const auto count = _pins.size();
		const auto currentWidth = current->userpic.width();
		const auto currentMiddle = current->x + currentWidth / 2;
		*currentDesiredIndex = index;
		if (shift > 0) {
			auto top = current->x - shift;
			for (auto next = index + 1; next != count; ++next) {
				const auto &entry = _pins[next];
				top += entry->userpic.width();
				if (currentMiddle < top) {
					moveToShift(next, 0);
				} else {
					*currentDesiredIndex = next;
					moveToShift(next, -currentWidth);
				}
			}
			for (auto prev = index - 1; prev >= 0; --prev) {
				moveToShift(prev, 0);
			}
		} else {
			for (auto next = index + 1; next != count; ++next) {
				moveToShift(next, 0);
			}
			for (auto prev = index - 1; prev >= 0; --prev) {
				const auto &entry = _pins[prev];
				if (currentMiddle >= entry->x - entry->shift + currentWidth) {
					moveToShift(prev, 0);
				} else {
					*currentDesiredIndex = prev;
					moveToShift(prev, currentWidth);
				}
			}
		}
	};

	const auto checkForStart = [=](int positionX) {
		const auto shift = positionX - *currentStart;
		const auto delta = QApplication::startDragDistance();
		*isDragging = (std::abs(shift) > delta);
		if (!*isDragging) {
			return;
		}

		*currentState = State::Started;
		*currentStart += (shift > 0) ? delta : -delta;

		const auto index = indexOf(*currentPeer);
		*currentDesiredIndex = index;

		// Raise the pin.
		ranges::for_each(_pins, [=](const auto &pin) {
			pin->onTop = false;
		});
		_pins[index]->onTop = true;

		updateOrder(index, positionX);
	};

	const auto localGuard = _lifetime.make_state<base::has_weak_ptr>();

	const auto finishCurrent = [=] {
		if (!*currentPeer) {
			return;
		}
		const auto index = indexOf(*currentPeer);
		if (*currentDesiredIndex == index
			|| *currentState != State::Started) {
			cancelCurrentPeer();
			return;
		}
		const auto result = *currentDesiredIndex;
		*currentState = State::Cancelled;
		*currentPeer = nullptr;

		const auto &current = _pins[index];
		// Since the width of all elements is the same
		// we can use a single value.
		current->finalShift += (index - result) * current->userpic.width();

		if (!(current->finalShift + current->deltaShift)) {
			current->shift = 0;
			setHorizontalShift(current, 0);
		}
		current->horizontalShift = current->finalShift;
		base::reorder(_pins, index, result);

		*waitForFinish = true;
		// Call on end of an animation.
		base::call_delayed(st::slideWrapDuration, &(*localGuard), [=] {
			const auto guard = gsl::finally([=] {
				_session->data().notifyPinnedDialogsOrderUpdated();
				*waitForFinish = false;
			});
			if (index == result) {
				return;
			}
			const auto &order = _session->data().pinnedChatsOrder(nullptr);
			const auto d = (index < result) ? 1 : -1; // Direction.
			for (auto i = index; i != result; i += d) {
				_session->data().chatsList()->pinned()->reorder(
					order.at(i).history(),
					order.at(i + d).history());
			}
			_session->api().savePinnedOrder(nullptr);
		});

		moveToShift(result, 0);
	};

	const auto touchBegan = [=](int touchX) {
		*isDragging = false;
		cancelCurrent();
		*currentStart = touchX;
		if (_pins.size() < 2) {
			return;
		}
		const auto index = [self indexFromX:*currentStart];
		if (index < 0) {
			return;
		}
		*currentPeer = _pins[index]->peer;
	};

	const auto touchMoved = [=](int touchX) {
		if (!*currentPeer) {
			return;
		} else if (*currentState != State::Started) {
			checkForStart(touchX);
		} else {
			updateOrder(indexOf(*currentPeer), touchX);
		}
	};

	const auto touchEnded = [=](int touchX) {
		if (*isDragging) {
			finishCurrent();
			return;
		}
		const auto step = QApplication::startDragDistance();
		if (std::abs(*currentStart - touchX) < step) {
			[self performAction:touchX];
		}
	};

	_gestures.events(
	) | rpl::filter([=] {
		return !(*waitForFinish);
	}) | rpl::start_with_next([=](
			not_null<NSPressGestureRecognizer*> gesture) {
		const auto currentPosition = [gesture locationInView:self].x;

		switch ([gesture state]) {
		case NSGestureRecognizerStateBegan:
			return touchBegan(currentPosition);
		case NSGestureRecognizerStateChanged:
			return touchMoved(currentPosition);
		case NSGestureRecognizerStateCancelled:
		case NSGestureRecognizerStateEnded:
			return touchEnded(currentPosition);
		}
	}, _lifetime);

	_session->data().pinnedDialogsOrderUpdated(
	) | rpl::start_with_next(cancelCurrent, _lifetime);

	_lifetime.add([=] {
		for (const auto &pin : _pins) {
			pin->shiftAnimation.stop();
			pin->onlineAnimation.stop();
		}
	});

}

- (id)init:(not_null<Main::Session*>)session
		destroyEvent:(rpl::producer<>)touchBarSwitches {
	self = [super init];
	_session = session;
	_hasArchive = _selfUnpinned = false;
	_savedMessages = SavedMessagesUserpic();
	_repliesMessages = RepliesMessagesUserpic();

	auto *gesture = [[[NSPressGestureRecognizer alloc]
		initWithTarget:self
		action:@selector(gestureHandler:)] autorelease];
	gesture.allowedTouchTypes = NSTouchTypeMaskDirect;
	gesture.minimumPressDuration = 0;
	gesture.allowableMovement = 0;
	[self addGestureRecognizer:gesture];

	// For some reason, sometimes a parent deallocates not immediately,
	// but only after the user's input (mouse movement, key pressing, etc.).
	// So we have to use a custom event to destroy the current lifetime
	// manually, before it leads to crashes.
	std::move(
		touchBarSwitches
	) | rpl::start_with_next([=] {
		_lifetime.destroy();
	}, _lifetime);

	using UpdateFlag = Data::PeerUpdate::Flag;

	const auto downloadLifetime = _lifetime.make_state<rpl::lifetime>();
	const auto peerChangedLifetime = _lifetime.make_state<rpl::lifetime>();
	const auto lastDialogsCount = _lifetime.make_state<rpl::variable<int>>(0);
	auto &&peers = ranges::views::all(
		_pins
	) | ranges::views::transform(&Pin::peer);

	const auto updatePanelSize = [=] {
		const auto size = lastDialogsCount->current();
		if (self.image) {
			[self.image release];
		}
		// TODO: replace it with NSLayoutConstraint.
		self.image = [[NSImage alloc] initWithSize:NSMakeSize(
			size * (kCircleDiameter + kPinnedButtonsSpace)
				+ kPinnedButtonsLeftSkip
				- kPinnedButtonsSpace / 2,
			kCircleDiameter)];
	};
	lastDialogsCount->changes(
	) | rpl::start_with_next(updatePanelSize, _lifetime);
	const auto singleUserpic = [=](const auto &pin) {
		if (IsSelfPeer(pin->peer)) {
			pin->userpic = _savedMessages;
			return;
		} else if (IsRepliesPeer(pin->peer)) {
			pin->userpic = _repliesMessages;
			return;
		}
		auto userpic = PrepareImage();
		Painter p(&userpic);

		pin->peer->paintUserpic(p, pin->userpicView, 0, 0, userpic.width());
		userpic.setDevicePixelRatio(style::DevicePixelRatio());
		pin->userpic = std::move(userpic);
		const auto userpicIndex = pin->index + [self shift];
		[self setNeedsDisplayInRect:PeerRectByIndex(userpicIndex)];
	};
	const auto updateUserpics = [=] {
		ranges::for_each(_pins, singleUserpic);
		*lastDialogsCount = [self shift] + int(std::size(_pins));
	};
	const auto updateBadge = [=](const auto &pin) {
		const auto peer = pin->peer;
		if (IsSelfPeer(peer)) {
			return;
		}
		pin->unreadBadge = UnreadBadge(peer);

		const auto userpicIndex = pin->index + [self shift];
		[self setNeedsDisplayInRect:PeerRectByIndex(userpicIndex)];
	};
	const auto listenToDownloaderFinished = [=] {
		_session->downloaderTaskFinished(
		) | rpl::start_with_next([=] {
			const auto all = ranges::all_of(_pins, [=](const auto &pin) {
				return (!pin->peer->hasUserpic())
					|| (!Ui::PeerUserpicLoading(pin->userpicView));
			});
			if (all) {
				downloadLifetime->destroy();
			}
			updateUserpics();
		}, *downloadLifetime);
	};
	const auto processOnline = [=](const auto &pin) {
		// TODO: this should be replaced
		// with the global application timer for online statuses.
		const auto onlineChanges
			= peerChangedLifetime->make_state<rpl::event_stream<PeerData*>>();
		const auto peer = pin->peer;
		const auto onlineTimer = peerChangedLifetime->make_state<base::Timer>(
			[=] { onlineChanges->fire_copy({ peer }); });

		const auto callTimer = [=](const auto &pin) {
			onlineTimer->cancel();
			if (const auto till = pin->lastseen.onlineTill()) {
				const auto left = till - base::unixtime::now();
				if (left > 0) {
					onlineTimer->callOnce(std::min(86400, left)
						* crl::time(1000));
				}
			}
		};
		callTimer(pin);

		using PeerUpdate = Data::PeerUpdate;
		auto to_peer = rpl::map([=](const PeerUpdate &update) -> PeerData* {
			return update.peer;
		});
		rpl::merge(
			_session->changes().peerUpdates(
				pin->peer,
				UpdateFlag::OnlineStatus) | to_peer,
			onlineChanges->events()
		) | rpl::start_with_next([=](PeerData *peer) {
			const auto it = ranges::find(_pins, peer, &Pin::peer);
			if (it == end(_pins)) {
				return;
			}
			const auto &pin = *it;
			pin->lastseen = CalculateLastseenStatus(pin->peer);

			callTimer(pin);

			if (![NSApplication sharedApplication].active) {
				pin->onlineAnimation.stop();
				return;
			}
			const auto now = base::unixtime::now();
			const auto online = pin->lastseen.isOnline(now);
			if (pin->onlineAnimation.animating()) {
				pin->onlineAnimation.change(
					online ? 1. : 0.,
					st::dialogsOnlineBadgeDuration);
			} else {
				const auto s = kOnlineCircleSize + kOnlineCircleStrokeWidth;
				const auto index = pin->index;
				Core::Sandbox::Instance().customEnterFromEventLoop([=] {
					_pins[index]->onlineAnimation.start(
						[=] {
							[self setNeedsDisplayInRect:NSMakeRect(
								_pins[index]->x + kCircleDiameter - s,
								0,
								s,
								s)];
						},
						online ? 0. : 1.,
						online ? 1. : 0.,
						st::dialogsOnlineBadgeDuration);
				});
			}
		}, *peerChangedLifetime);
	};

	const auto updatePinnedChats = [=] {
		_pins = ranges::views::zip(
			_session->data().pinnedChatsOrder(nullptr),
			ranges::views::ints(0, ranges::unreachable)
		) | ranges::views::transform([=](const auto &pair) {
			const auto index = pair.second;
			auto peer = pair.first.history()->peer;
			auto view = peer->createUserpicView();
			return std::make_unique<Pin>(Pin{
				.peer = std::move(peer),
				.userpicView = std::move(view),
				.index = index,
				.lastseen = CalculateLastseenStatus(peer),
			});
		}) | ranges::to_vector;
		_selfUnpinned = ranges::none_of(peers, &PeerData::isSelf);
		_repliesUnpinned = ranges::none_of(peers, &PeerData::isRepliesChat);

		peerChangedLifetime->destroy();
		for (const auto &pin : _pins) {
			const auto peer = pin->peer;
			const auto index = pin->index;

			_session->changes().peerUpdates(
				peer,
				UpdateFlag::Photo
			) | rpl::start_with_next([=](const Data::PeerUpdate &update) {
				_pins[index]->userpicView = update.peer->createUserpicView();
				listenToDownloaderFinished();
			}, *peerChangedLifetime);

			if (const auto user = peer->asUser()) {
				if (!user->isServiceUser()
					&& !user->isBot()
					&& !peer->isSelf()) {
					processOnline(pin);
				}
			}

			rpl::merge(
				_session->changes().historyUpdates(
					_session->data().history(peer),
					Data::HistoryUpdate::Flag::UnreadView
				) | rpl::to_empty,
				_session->changes().peerFlagsValue(
					peer,
					UpdateFlag::Notifications
				) | rpl::to_empty
			) | rpl::start_with_next([=] {
				updateBadge(_pins[index]);
			}, *peerChangedLifetime);
		}

		updateUserpics();
	};

	rpl::single(rpl::empty) | rpl::then(
		_session->data().pinnedDialogsOrderUpdated()
	) | rpl::start_with_next(updatePinnedChats, _lifetime);

	const auto ArchiveId = Data::Folder::kId;
	rpl::single(
		_session->data().folderLoaded(ArchiveId)
	) | rpl::then(
		_session->data().chatsListChanges()
	) | rpl::filter([](Data::Folder *folder) {
		return folder && (folder->id() == ArchiveId);
	}) | rpl::start_with_next([=](Data::Folder *folder) {
		_hasArchive = !folder->chatsList()->empty();
		if (_archive.isNull()) {
			_archive = ArchiveUserpic(folder);
		}
		updateUserpics();
	}, _lifetime);

	const auto updateOnlineColor = [=] {
		auto r = 0, g = 0, b = 0, a = 0;
		st::dialogsOnlineBadgeFg->c.getRgb(&r, &g, &b, &a);
		_r = r / 255.;
		_g = g / 255.;
		_b = b / 255.;
		_a = a / 255.;
	};
	updateOnlineColor();

	const auto localGuard = _lifetime.make_state<base::has_weak_ptr>();

	style::PaletteChanged(
	) | rpl::start_with_next([=] {
		crl::on_main(&(*localGuard), [=] {
			updateOnlineColor();
			if (const auto f = _session->data().folderLoaded(ArchiveId)) {
				_archive = ArchiveUserpic(f);
			}
			_savedMessages = SavedMessagesUserpic();
			_repliesMessages = RepliesMessagesUserpic();
			updateUserpics();
		});
	}, _lifetime);

	listenToDownloaderFinished();
	[self processHorizontalReorder];
	return self;
}

- (void)dealloc {
	if (self.image) {
		[self.image release];
	}
	[super dealloc];
}

- (int)shift {
	return (_hasArchive ? 1 : 0) + (_selfUnpinned ? 1 : 0);
}

- (void)gestureHandler:(NSPressGestureRecognizer*)gesture {
	_gestures.fire(std::move(gesture));
}

- (int)indexFromX:(int)position {
	const auto x = position
		- kPinnedButtonsLeftSkip
		+ kPinnedButtonsSpace / 2;
	return x / (kCircleDiameter + kPinnedButtonsSpace) - [self shift];
}

- (void)performAction:(int)xPosition {
	const auto index = [self indexFromX:xPosition];
	const auto peer = (index < 0 || index >= int(std::size(_pins)))
		? nullptr
		: _pins[index]->peer;
	if (!peer && !_hasArchive && !_selfUnpinned) {
		return;
	}

	const auto active = Core::App().activePrimaryWindow();
	const auto controller = active ? active->sessionController() : nullptr;
	const auto openFolder = [=] {
		const auto folder = _session->data().folderLoaded(Data::Folder::kId);
		if (folder && controller) {
			controller->openFolder(folder);
		}
	};
	Core::Sandbox::Instance().customEnterFromEventLoop([=] {
		if (_hasArchive && (index == (_selfUnpinned ? -2 : -1))) {
			openFolder();
		} else {
			controller->showPeerHistory((_selfUnpinned && index == -1)
				? _session->user()
				: peer);
		}
	});
}

- (QImage)imageToDraw:(int)i {
	Expects(i < int(std::size(_pins)));
	if (i < 0) {
		if (_hasArchive && (i == -[self shift])) {
			return _archive;
		} else if (_selfUnpinned) {
			return _savedMessages;
		} else if (_repliesUnpinned) {
			return _repliesMessages;
		}
	}
	return _pins[i]->userpic;
}

- (void)drawSinglePin:(int)i rect:(NSRect)dirtyRect {
	const auto rect = [&] {
		auto rect = PeerRectByIndex(i + [self shift]);
		if (i < 0) {
			return rect;
		}
		auto &pin = _pins[i];
		// We can have x = 0 when the pin is dragged.
		rect.origin.x = ((!pin->x && !pin->onTop) ? rect.origin.x : pin->x);
		pin->x = rect.origin.x;
		return rect;
	}();
	if (!NSIntersectsRect(rect, dirtyRect)) {
		return;
	}
	CGContextRef context = [[NSGraphicsContext currentContext] CGContext];
	{
		CGImageRef image = ([self imageToDraw:i]).toCGImage();
		CGContextDrawImage(context, rect, image);
		CGImageRelease(image);
	}

	if (i >= 0) {
		const auto &pin = _pins[i];
		const auto rectRight = NSMaxX(rect);
		if (!pin->unreadBadge.isNull()) {
			CGImageRef image = pin->unreadBadge.toCGImage();
			const auto w = CGImageGetWidth(image)
				/ float64(style::DevicePixelRatio());
			const auto borderRect = CGRectMake(
				rectRight - w,
				0,
				w,
				CGImageGetHeight(image)
					/ float64(style::DevicePixelRatio()));
			CGContextDrawImage(context, borderRect, image);
			CGImageRelease(image);
			return;
		}
		const auto now = base::unixtime::now();
		const auto online = pin->lastseen.isOnline(now);
		const auto value = pin->onlineAnimation.value(online ? 1. : 0.);
		if (value < 0.05) {
			return;
		}
		const auto lineWidth = kOnlineCircleStrokeWidth;
		const auto circleSize = kOnlineCircleSize;
		const auto progress = value * circleSize;
		const auto diff = (circleSize - progress) / 2;
		const auto borderRect = CGRectMake(
			rectRight - circleSize + diff - lineWidth / 2,
			diff,
			progress,
			progress);

		CGContextSetRGBStrokeColor(context, 0, 0, 0, 1.0);
		CGContextSetRGBFillColor(context, _r, _g, _b, _a);
		CGContextSetLineWidth(context, lineWidth);
		CGContextFillEllipseInRect(context, borderRect);
		CGContextStrokeEllipseInRect(context, borderRect);
	}
}

- (void)drawRect:(NSRect)dirtyRect {
	const auto shift = [self shift];
	if (_pins.empty() && !shift) {
		return;
	}
	auto indexToTop = -1;
	const auto guard = gsl::finally([&] {
		if (indexToTop >= 0) {
			[self drawSinglePin:indexToTop rect:dirtyRect];
		}
	});
	for (auto i = -shift; i < int(std::size(_pins)); i++) {
		if (i >= 0 && _pins[i]->onTop && (indexToTop < 0)) {
			indexToTop = i;
			continue;
		}
		[self drawSinglePin:i rect:dirtyRect];
	}
}

@end // @@implementation PinnedDialogsPanel
