/*
This file is part of Telegram Desktop,
the official desktop version of Telegram messaging app, see https://telegram.org

Telegram Desktop is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

It is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

In addition, as a special exception, the copyright holders give permission
to link the code of portions of this program with the OpenSSL library.

Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE
Copyright (c) 2014-2017 John Preston, https://desktop.telegram.org
*/
#pragma once

#include "base/runtime_composer.h"
#include "base/flags.h"
#include "base/value_ordering.h"

namespace base {
template <typename Enum>
class enum_mask;
} // namespace base

namespace Storage {
enum class SharedMediaType : char;
using SharedMediaTypesMask = base::enum_mask<SharedMediaType>;
} // namespace Storage

namespace Ui {
class RippleAnimation;
} // namespace Ui

namespace style {
struct BotKeyboardButton;
struct RippleAnimation;
} // namespace style

class HistoryElement {
public:
	HistoryElement() = default;
	HistoryElement(const HistoryElement &other) = delete;
	HistoryElement &operator=(const HistoryElement &other) = delete;

	int maxWidth() const {
		return _maxw;
	}
	int minHeight() const {
		return _minh;
	}
	int height() const {
		return _height;
	}

	virtual ~HistoryElement() = default;

protected:
	mutable int _maxw = 0;
	mutable int _minh = 0;
	mutable int _height = 0;

};

class HistoryMessage;

enum HistoryCursorState {
	HistoryDefaultCursorState,
	HistoryInTextCursorState,
	HistoryInDateCursorState,
	HistoryInForwardedCursorState,
};

struct HistoryTextState {
	HistoryTextState() = default;
	HistoryTextState(not_null<const HistoryItem*> item);
	HistoryTextState(
		not_null<const HistoryItem*> item,
		const Text::StateResult &state);
	HistoryTextState(
		not_null<const HistoryItem*> item,
		ClickHandlerPtr link);
	HistoryTextState(
		std::nullptr_t,
		const Text::StateResult &state)
	: cursor(state.uponSymbol
		? HistoryInTextCursorState
		: HistoryDefaultCursorState)
	, link(state.link)
	, afterSymbol(state.afterSymbol)
	, symbol(state.symbol) {
	}
	HistoryTextState(std::nullptr_t, ClickHandlerPtr link)
	: link(link) {
	}

	FullMsgId itemId;
	HistoryCursorState cursor = HistoryDefaultCursorState;
	ClickHandlerPtr link;
	bool afterSymbol = false;
	uint16 symbol = 0;

};

struct HistoryStateRequest {
	Text::StateRequest::Flags flags = Text::StateRequest::Flag::LookupLink;
	Text::StateRequest forText() const {
		Text::StateRequest result;
		result.flags = flags;
		return result;
	}
};

enum InfoDisplayType {
	InfoDisplayDefault,
	InfoDisplayOverImage,
	InfoDisplayOverBackground,
};

struct HistoryMessageVia : public RuntimeComponent<HistoryMessageVia> {
	void create(int32 userId);
	void resize(int32 availw) const;

	UserData *_bot = nullptr;
	mutable QString _text;
	mutable int _width = 0;
	mutable int _maxWidth = 0;
	ClickHandlerPtr _lnk;
};

struct HistoryMessageViews : public RuntimeComponent<HistoryMessageViews> {
	QString _viewsText;
	int _views = 0;
	int _viewsWidth = 0;
};

struct HistoryMessageSigned : public RuntimeComponent<HistoryMessageSigned> {
	void create(const QString &author, const QString &date);
	int maxWidth() const;

	QString _author;
	Text _signature;
};

struct HistoryMessageEdited : public RuntimeComponent<HistoryMessageEdited> {
	void refresh(const QString &date, bool displayed);
	int maxWidth() const;

	QDateTime date;
	Text text;
};

struct HistoryMessageForwarded : public RuntimeComponent<HistoryMessageForwarded> {
	void create(const HistoryMessageVia *via) const;

	QDateTime _originalDate;
	PeerData *_originalSender = nullptr;
	QString _originalAuthor;
	MsgId _originalId = 0;
	mutable Text _text = { 1 };

	PeerData *_savedFromPeer = nullptr;
	MsgId _savedFromMsgId = 0;
};

struct HistoryMessageReply : public RuntimeComponent<HistoryMessageReply> {
	HistoryMessageReply() = default;
	HistoryMessageReply(const HistoryMessageReply &other) = delete;
	HistoryMessageReply(HistoryMessageReply &&other) = delete;
	HistoryMessageReply &operator=(const HistoryMessageReply &other) = delete;
	HistoryMessageReply &operator=(HistoryMessageReply &&other) {
		replyToMsgId = other.replyToMsgId;
		std::swap(replyToMsg, other.replyToMsg);
		replyToLnk = std::move(other.replyToLnk);
		replyToName = std::move(other.replyToName);
		replyToText = std::move(other.replyToText);
		replyToVersion = other.replyToVersion;
		_maxReplyWidth = other._maxReplyWidth;
		_replyToVia = std::move(other._replyToVia);
		return *this;
	}
	~HistoryMessageReply() {
		// clearData() should be called by holder
		Expects(replyToMsg == nullptr);
		Expects(_replyToVia == nullptr);
	}

	bool updateData(HistoryMessage *holder, bool force = false);
	void clearData(HistoryMessage *holder); // must be called before destructor

	bool isNameUpdated() const;
	void updateName() const;
	void resize(int width) const;
	void itemRemoved(HistoryMessage *holder, HistoryItem *removed);

	enum class PaintFlag {
		InBubble = (1 << 0),
		Selected = (1 << 1),
	};
	using PaintFlags = base::flags<PaintFlag>;
	friend inline constexpr auto is_flag_type(PaintFlag) { return true; };
	void paint(Painter &p, const HistoryItem *holder, int x, int y, int w, PaintFlags flags) const;

	MsgId replyToId() const {
		return replyToMsgId;
	}
	int replyToWidth() const {
		return _maxReplyWidth;
	}
	ClickHandlerPtr replyToLink() const {
		return replyToLnk;
	}

	MsgId replyToMsgId = 0;
	HistoryItem *replyToMsg = nullptr;
	ClickHandlerPtr replyToLnk;
	mutable Text replyToName, replyToText;
	mutable int replyToVersion = 0;
	mutable int _maxReplyWidth = 0;
	std::unique_ptr<HistoryMessageVia> _replyToVia;
	int toWidth = 0;
};

class ReplyKeyboard;
struct HistoryMessageReplyMarkup : public RuntimeComponent<HistoryMessageReplyMarkup> {
	HistoryMessageReplyMarkup() = default;
	HistoryMessageReplyMarkup(MTPDreplyKeyboardMarkup::Flags f) : flags(f) {
	}

	void create(const MTPReplyMarkup &markup);
	void create(const HistoryMessageReplyMarkup &markup);

	struct Button {
		enum class Type {
			Default,
			Url,
			Callback,
			RequestPhone,
			RequestLocation,
			SwitchInline,
			SwitchInlineSame,
			Game,
			Buy,
		};
		Type type;
		QString text;
		QByteArray data;
		mutable mtpRequestId requestId;
	};
	using ButtonRow = QVector<Button>;
	using ButtonRows = QVector<ButtonRow>;

	ButtonRows rows;
	MTPDreplyKeyboardMarkup::Flags flags = 0;

	std::unique_ptr<ReplyKeyboard> inlineKeyboard;

	// If >= 0 it holds the y coord of the inlineKeyboard before the last edition.
	int oldTop = -1;

private:
	void createFromButtonRows(const QVector<MTPKeyboardButtonRow> &v);

};

class ReplyMarkupClickHandler : public LeftButtonClickHandler {
public:
	ReplyMarkupClickHandler(const HistoryItem *item, int row, int col);

	QString tooltip() const override {
		return _fullDisplayed ? QString() : buttonText();
	}

	void setFullDisplayed(bool full) {
		_fullDisplayed = full;
	}

	// Copy to clipboard support.
	void copyToClipboard() const override;
	QString copyToClipboardContextItemText() const override;

	// Finds the corresponding button in the items markup struct.
	// If the button is not found it returns nullptr.
	// Note: it is possible that we will point to the different button
	// than the one was used when constructing the handler, but not a big deal.
	const HistoryMessageReplyMarkup::Button *getButton() const;

	// We hold only FullMsgId, not HistoryItem*, because all click handlers
	// are activated async and the item may be already destroyed.
	void setMessageId(const FullMsgId &msgId) {
		_itemId = msgId;
	}

protected:
	void onClickImpl() const override;

private:
	FullMsgId _itemId;
	int _row, _col;
	bool _fullDisplayed = true;

	// Returns the full text of the corresponding button.
	QString buttonText() const;

};

class ReplyKeyboard {
private:
	struct Button;

public:
	class Style {
	public:
		Style(const style::BotKeyboardButton &st) : _st(&st) {
		}

		virtual void startPaint(Painter &p) const = 0;
		virtual const style::TextStyle &textStyle() const = 0;

		int buttonSkip() const;
		int buttonPadding() const;
		int buttonHeight() const;
		virtual int buttonRadius() const = 0;

		virtual void repaint(not_null<const HistoryItem*> item) const = 0;
		virtual ~Style() {
		}

	protected:
		virtual void paintButtonBg(Painter &p, const QRect &rect, float64 howMuchOver) const = 0;
		virtual void paintButtonIcon(Painter &p, const QRect &rect, int outerWidth, HistoryMessageReplyMarkup::Button::Type type) const = 0;
		virtual void paintButtonLoading(Painter &p, const QRect &rect) const = 0;
		virtual int minButtonWidth(HistoryMessageReplyMarkup::Button::Type type) const = 0;

	private:
		const style::BotKeyboardButton *_st;

		void paintButton(Painter &p, int outerWidth, const ReplyKeyboard::Button &button, TimeMs ms) const;
		friend class ReplyKeyboard;

	};
	typedef std::unique_ptr<Style> StylePtr;

	ReplyKeyboard(const HistoryItem *item, StylePtr &&s);
	ReplyKeyboard(const ReplyKeyboard &other) = delete;
	ReplyKeyboard &operator=(const ReplyKeyboard &other) = delete;

	bool isEnoughSpace(int width, const style::BotKeyboardButton &st) const;
	void setStyle(StylePtr &&s);
	void resize(int width, int height);

	// what width and height will best fit this keyboard
	int naturalWidth() const;
	int naturalHeight() const;

	void paint(Painter &p, int outerWidth, const QRect &clip, TimeMs ms) const;
	ClickHandlerPtr getState(QPoint point) const;

	void clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active);
	void clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed);

	void clearSelection();
	void updateMessageId();

private:
	void startAnimation(int i, int j, int direction);

	friend class Style;
	using ReplyMarkupClickHandlerPtr = QSharedPointer<ReplyMarkupClickHandler>;
	struct Button {
		Text text = { 1 };
		QRect rect;
		int characters = 0;
		float64 howMuchOver = 0.;
		HistoryMessageReplyMarkup::Button::Type type;
		ReplyMarkupClickHandlerPtr link;
		mutable QSharedPointer<Ui::RippleAnimation> ripple;
	};
	using ButtonRow = QVector<Button>;
	using ButtonRows = QVector<ButtonRow>;

	struct ButtonCoords {
		int i, j;
	};
	ButtonCoords findButtonCoordsByClickHandler(const ClickHandlerPtr &p);

	using Animations = QMap<int, TimeMs>;
	void step_selected(TimeMs ms, bool timer);

	const HistoryItem *_item;
	int _width = 0;

	ButtonRows _rows;

	Animations _animations;
	BasicAnimation _a_selected;

	StylePtr _st;

	ClickHandlerPtr _savedPressed;
	ClickHandlerPtr _savedActive;
	mutable QPoint _savedCoords;

};

// Any HistoryItem can have this Component for
// displaying the day mark above the message.
struct HistoryMessageDate : public RuntimeComponent<HistoryMessageDate> {
	void init(const QDateTime &date);

	int height() const;
	void paint(Painter &p, int y, int w) const;

	QString _text;
	int _width = 0;
};

// Any HistoryItem can have this Component for
// displaying the unread messages bar above the message.
struct HistoryMessageUnreadBar : public RuntimeComponent<HistoryMessageUnreadBar> {
	void init(int count);

	static int height();
	static int marginTop();

	void paint(Painter &p, int y, int w) const;

	QString _text;
	int _width = 0;

	// If unread bar is freezed the new messages do not
	// increment the counter displayed by this bar.
	//
	// It happens when we've opened the conversation and
	// we've seen the bar and new messages are marked as read
	// as soon as they are added to the chat history.
	bool _freezed = false;

};

struct MessageGroupId {
	using Underlying = uint64;

	enum Type : Underlying {
		None = 0,
	} value;

	MessageGroupId(Type value = None) : value(value) {
	}
	static MessageGroupId FromRaw(Underlying value) {
		return static_cast<Type>(value);
	}

	explicit operator bool() const {
		return value != None;
	}

	friend inline Type value_ordering_helper(MessageGroupId value) {
		return value.value;
	}

};
struct HistoryMessageGroup : public RuntimeComponent<HistoryMessageGroup> {
	MessageGroupId groupId = MessageGroupId::None;
	HistoryItem *leader = nullptr;
	std::vector<not_null<HistoryItem*>> others;
};

class HistoryWebPage;

// Special type of Component for the channel actions log.
struct HistoryMessageLogEntryOriginal : public RuntimeComponent<HistoryMessageLogEntryOriginal> {
	HistoryMessageLogEntryOriginal();
	HistoryMessageLogEntryOriginal(HistoryMessageLogEntryOriginal &&other);
	HistoryMessageLogEntryOriginal &operator=(HistoryMessageLogEntryOriginal &&other);
	~HistoryMessageLogEntryOriginal();

	std::unique_ptr<HistoryWebPage> _page;

};

// HistoryMedia has a special owning smart pointer
// which regs/unregs this media to the holding HistoryItem
class HistoryMedia;
class HistoryMediaPtr {
public:
	HistoryMediaPtr();
	HistoryMediaPtr(const HistoryMediaPtr &other) = delete;
	HistoryMediaPtr &operator=(const HistoryMediaPtr &other) = delete;
	HistoryMediaPtr(std::unique_ptr<HistoryMedia> other);
	HistoryMediaPtr &operator=(std::unique_ptr<HistoryMedia> other);

	HistoryMedia *get() const {
		return _pointer.get();
	}
	void reset(std::unique_ptr<HistoryMedia> pointer = nullptr);
	bool isNull() const {
		return !_pointer;
	}

	HistoryMedia *operator->() const {
		return get();
	}
	HistoryMedia &operator*() const {
		Expects(!isNull());
		return *get();
	}
	explicit operator bool() const {
		return !isNull();
	}
	~HistoryMediaPtr();

private:
	std::unique_ptr<HistoryMedia> _pointer;

};

namespace internal {

TextSelection unshiftSelection(TextSelection selection, uint16 byLength);
TextSelection shiftSelection(TextSelection selection, uint16 byLength);
inline TextSelection unshiftSelection(TextSelection selection, const Text &byText) {
	return ::internal::unshiftSelection(selection, byText.length());
}
inline TextSelection shiftSelection(TextSelection selection, const Text &byText) {
	return ::internal::shiftSelection(selection, byText.length());
}

} // namespace internal

class HistoryItem : public HistoryElement, public RuntimeComposer, public ClickHandlerHost {
public:
	int resizeGetHeight(int newWidth) {
		if (_flags & MTPDmessage_ClientFlag::f_pending_init_dimensions) {
			_flags &= ~MTPDmessage_ClientFlag::f_pending_init_dimensions;
			initDimensions();
		}
		if (_flags & MTPDmessage_ClientFlag::f_pending_resize) {
			_flags &= ~MTPDmessage_ClientFlag::f_pending_resize;
		}
		_width = newWidth;
		return resizeContentGetHeight();
	}
	virtual void draw(Painter &p, QRect clip, TextSelection selection, TimeMs ms) const = 0;

	virtual void dependencyItemRemoved(HistoryItem *dependency) {
	}
	virtual bool updateDependencyItem() {
		return true;
	}
	virtual MsgId dependencyMsgId() const {
		return 0;
	}
	virtual bool notificationReady() const {
		return true;
	}
	virtual void applyGroupAdminChanges(
		const base::flat_map<UserId, bool> &changes) {
	}

	UserData *viaBot() const {
		if (auto via = Get<HistoryMessageVia>()) {
			return via->_bot;
		}
		return nullptr;
	}
	UserData *getMessageBot() const {
		if (auto bot = viaBot()) {
			return bot;
		}
		auto bot = from()->asUser();
		if (!bot) {
			bot = history()->peer->asUser();
		}
		return (bot && bot->botInfo) ? bot : nullptr;
	};

	bool isLogEntry() const {
		return (id > ServerMaxMsgId);
	}
	void addLogEntryOriginal(WebPageId localId, const QString &label, const TextWithEntities &content);

	not_null<History*> history() const {
		return _history;
	}
	PeerData *from() const {
		return _from;
	}
	HistoryBlock *block() {
		return _block;
	}
	const HistoryBlock *block() const {
		return _block;
	}
	void destroy();
	void detach();
	void detachFast();
	bool detached() const {
		return !_block;
	}
	void attachToBlock(HistoryBlock *block, int index) {
		Expects(!isLogEntry());
		Expects(_block == nullptr);
		Expects(_indexInBlock < 0);
		Expects(block != nullptr);
		Expects(index >= 0);

		_block = block;
		_indexInBlock = index;
		setPendingResize();
	}
	void setIndexInBlock(int index) {
		Expects(_block != nullptr);
		Expects(index >= 0);

		_indexInBlock = index;
	}
	int indexInBlock() const {
		Expects((_indexInBlock >= 0) == (_block != nullptr));
		Expects((_block == nullptr) || (_block->items[_indexInBlock] == this));

		return _indexInBlock;
	}
	bool out() const {
		return _flags & MTPDmessage::Flag::f_out;
	}
	bool unread() const;
	bool mentionsMe() const {
		return _flags & MTPDmessage::Flag::f_mentioned;
	}
	bool isMediaUnread() const;
	void markMediaRead();

	// Zero result means this message is not self-destructing right now.
	virtual TimeMs getSelfDestructIn(TimeMs now) {
		return 0;
	}

	bool definesReplyKeyboard() const {
		if (auto markup = Get<HistoryMessageReplyMarkup>()) {
			if (markup->flags & MTPDreplyKeyboardMarkup_ClientFlag::f_inline) {
				return false;
			}
			return true;
		}

		// optimization: don't create markup component for the case
		// MTPDreplyKeyboardHide with flags = 0, assume it has f_zero flag
		return (_flags & MTPDmessage::Flag::f_reply_markup);
	}
	MTPDreplyKeyboardMarkup::Flags replyKeyboardFlags() const {
		Expects(definesReplyKeyboard());
		if (auto markup = Get<HistoryMessageReplyMarkup>()) {
			return markup->flags;
		}

		// optimization: don't create markup component for the case
		// MTPDreplyKeyboardHide with flags = 0, assume it has f_zero flag
		return MTPDreplyKeyboardMarkup_ClientFlag::f_zero | 0;
	}
	bool hasSwitchInlineButton() const {
		return _flags & MTPDmessage_ClientFlag::f_has_switch_inline_button;
	}
	bool hasTextLinks() const {
		return _flags & MTPDmessage_ClientFlag::f_has_text_links;
	}
	bool isGroupMigrate() const {
		return _flags & MTPDmessage_ClientFlag::f_is_group_migrate;
	}
	bool hasViews() const {
		return _flags & MTPDmessage::Flag::f_views;
	}
	bool isPost() const {
		return _flags & MTPDmessage::Flag::f_post;
	}
	bool indexInUnreadMentions() const {
		return (id > 0);
	}
	bool isSilent() const {
		return _flags & MTPDmessage::Flag::f_silent;
	}
	bool hasOutLayout() const;
	virtual int32 viewsCount() const {
		return hasViews() ? 1 : -1;
	}

	virtual bool needCheck() const {
		return out() || (id < 0 && history()->peer->isSelf());
	}
	virtual bool hasPoint(QPoint point) const {
		return false;
	}

	[[nodiscard]] virtual HistoryTextState getState(
		QPoint point,
		HistoryStateRequest request) const = 0;
	virtual void updatePressed(QPoint point) {
	}

	[[nodiscard]] virtual TextSelection adjustSelection(
			TextSelection selection,
			TextSelectType type) const {
		return selection;
	}

	// ClickHandlerHost interface
	void clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) override;
	void clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) override;

	virtual bool serviceMsg() const {
		return false;
	}
	virtual void applyEdition(const MTPDmessage &message) {
	}
	virtual void applyEdition(const MTPDmessageService &message) {
	}
	virtual void updateMedia(const MTPMessageMedia *media) {
	}
	virtual void updateReplyMarkup(const MTPReplyMarkup *markup) {
	}

	virtual void addToUnreadMentions(AddToUnreadMentionsMethod method) {
	}
	virtual void eraseFromUnreadMentions() {
	}
	virtual Storage::SharedMediaTypesMask sharedMediaTypes() const;

	virtual bool hasBubble() const {
		return false;
	}

	void previousItemChanged();
	void nextItemChanged();

	virtual TextWithEntities selectedText(TextSelection selection) const {
		return { qsl("[-]"), EntitiesInText() };
	}

	virtual QString notificationHeader() const {
		return QString();
	}
	virtual QString notificationText() const;

	enum class DrawInDialog {
		Normal,
		WithoutSender,
	};

	// Returns text with link-start and link-end commands for service-color highlighting.
	// Example: "[link1-start]You:[link1-end] [link1-start]Photo,[link1-end] caption text"
	virtual QString inDialogsText(DrawInDialog way) const;
	virtual QString inReplyText() const {
		return notificationText();
	}
	virtual TextWithEntities originalText() const {
		return { QString(), EntitiesInText() };
	}

	virtual void drawInfo(Painter &p, int32 right, int32 bottom, int32 width, bool selected, InfoDisplayType type) const {
	}
	virtual ClickHandlerPtr rightActionLink() const {
		return ClickHandlerPtr();
	}
	virtual bool displayRightAction() const {
		return false;
	}
	virtual void drawRightAction(Painter &p, int left, int top, int outerWidth) const {
	}
	virtual void setViewsCount(int32 count) {
	}
	virtual void setId(MsgId newId);

	virtual bool displayEditedBadge() const {
		return false;
	}
	virtual QDateTime displayedEditDate() const {
		return QDateTime();
	}
	virtual void refreshEditedBadge() {
	}

	void drawInDialog(
		Painter &p,
		const QRect &r,
		bool active,
		bool selected,
		DrawInDialog way,
		const HistoryItem *&cacheFor,
		Text &cache) const;

	bool emptyText() const {
		return _text.isEmpty();
	}

	bool isPinned() const;
	bool canPin() const;
	bool canForward() const;
	bool canEdit(const QDateTime &cur) const;
	bool canDelete() const;
	bool canDeleteForEveryone(const QDateTime &cur) const;
	bool suggestBanReport() const;
	bool suggestDeleteAllReport() const;

	bool hasDirectLink() const;
	QString directLink() const;

	int y() const {
		return _y;
	}
	void setY(int y) {
		_y = y;
	}
	MsgId id;
	QDateTime date;

	ChannelId channelId() const {
		return _history->channelId();
	}
	FullMsgId fullId() const {
		return FullMsgId(channelId(), id);
	}

	HistoryMedia *getMedia() const {
		return _media.get();
	}
	virtual void setText(const TextWithEntities &textWithEntities) {
	}
	virtual bool textHasLinks() const {
		return false;
	}

	virtual int infoWidth() const {
		return 0;
	}
	virtual int timeLeft() const {
		return 0;
	}
	virtual int timeWidth() const {
		return 0;
	}
	virtual bool pointInTime(int right, int bottom, QPoint point, InfoDisplayType type) const {
		return false;
	}

	int skipBlockWidth() const {
		return st::msgDateSpace + infoWidth() - st::msgDateDelta.x();
	}
	int skipBlockHeight() const {
		return st::msgDateFont->height - st::msgDateDelta.y();
	}
	QString skipBlock() const {
		return textcmdSkipBlock(skipBlockWidth(), skipBlockHeight());
	}

	virtual HistoryMessage *toHistoryMessage() { // dynamic_cast optimize
		return nullptr;
	}
	virtual const HistoryMessage *toHistoryMessage() const { // dynamic_cast optimize
		return nullptr;
	}
	MsgId replyToId() const {
		if (auto reply = Get<HistoryMessageReply>()) {
			return reply->replyToId();
		}
		return 0;
	}

	PeerData *author() const {
		return isPost() ? history()->peer : from();
	}

	QDateTime dateOriginal() const {
		if (auto forwarded = Get<HistoryMessageForwarded>()) {
			return forwarded->_originalDate;
		}
		return date;
	}
	PeerData *senderOriginal() const {
		if (auto forwarded = Get<HistoryMessageForwarded>()) {
			return forwarded->_originalSender;
		}
		auto peer = history()->peer;
		return (peer->isChannel() && !peer->isMegagroup()) ? peer : from();
	}
	PeerData *fromOriginal() const {
		if (auto forwarded = Get<HistoryMessageForwarded>()) {
			if (auto user = forwarded->_originalSender->asUser()) {
				return user;
			}
		}
		return from();
	}
	QString authorOriginal() const {
		if (auto forwarded = Get<HistoryMessageForwarded>()) {
			return forwarded->_originalAuthor;
		} else if (auto msgsigned = Get<HistoryMessageSigned>()) {
			return msgsigned->_author;
		}
		return QString();
	}
	MsgId idOriginal() const {
		if (auto forwarded = Get<HistoryMessageForwarded>()) {
			return forwarded->_originalId;
		}
		return id;
	}

	// count > 0 - creates the unread bar if necessary and
	// sets unread messages count if bar is not freezed yet
	// count <= 0 - destroys the unread bar
	void setUnreadBarCount(int count);
	void destroyUnreadBar();

	// marks the unread bar as freezed so that unread
	// messages count will not change for this bar
	// when the new messages arrive in this chat history
	void setUnreadBarFreezed();

	bool pendingResize() const {
		return _flags & MTPDmessage_ClientFlag::f_pending_resize;
	}
	void setPendingResize() {
		_flags |= MTPDmessage_ClientFlag::f_pending_resize;
		if (!detached() || isLogEntry()) {
			_history->setHasPendingResizedItems();
		}
	}
	bool pendingInitDimensions() const {
		return _flags & MTPDmessage_ClientFlag::f_pending_init_dimensions;
	}
	void setPendingInitDimensions() {
		_flags |= MTPDmessage_ClientFlag::f_pending_init_dimensions;
		setPendingResize();
	}

	int displayedDateHeight() const {
		if (auto date = Get<HistoryMessageDate>()) {
			return date->height();
		}
		return 0;
	}
	int marginTop() const;
	int marginBottom() const;
	bool isAttachedToPrevious() const {
		return _flags & MTPDmessage_ClientFlag::f_attach_to_previous;
	}
	bool isAttachedToNext() const {
		return _flags & MTPDmessage_ClientFlag::f_attach_to_next;
	}
	bool displayDate() const {
		return Has<HistoryMessageDate>();
	}

	bool isInOneDayWithPrevious() const {
		return !isEmpty() && !displayDate();
	}

	bool isEmpty() const {
		return _text.isEmpty() && !_media && !Has<HistoryMessageLogEntryOriginal>();
	}
	bool isHiddenByGroup() const {
		return _flags & MTPDmessage_ClientFlag::f_hidden_by_group;
	}

	MessageGroupId groupId() const {
		if (const auto group = Get<HistoryMessageGroup>()) {
			return group->groupId;
		}
		return MessageGroupId::None;
	}
	bool groupIdValidityChanged();
	void validateGroupId() {
		// Just ignore the result.
		groupIdValidityChanged();
	}
	void makeGroupMember(not_null<HistoryItem*> leader);
	void makeGroupLeader(std::vector<not_null<HistoryItem*>> &&others);
	HistoryMessageGroup *getFullGroup();

	int width() const {
		return _width;
	}

	void clipCallback(Media::Clip::Notification notification);
	void audioTrackUpdated();

	bool computeIsAttachToPrevious(not_null<HistoryItem*> previous);
	void setLogEntryDisplayDate(bool displayDate) {
		Expects(isLogEntry());
		setDisplayDate(displayDate);
	}
	void setLogEntryAttachToPrevious(bool attachToPrevious) {
		Expects(isLogEntry());
		setAttachToPrevious(attachToPrevious);
	}
	void setLogEntryAttachToNext(bool attachToNext) {
		Expects(isLogEntry());
		setAttachToNext(attachToNext);
	}

	~HistoryItem();

protected:
	HistoryItem(
		not_null<History*> history,
		MsgId id,
		MTPDmessage::Flags flags,
		QDateTime date,
		UserId from);

	// To completely create history item we need to call
	// a virtual method, it can not be done from constructor.
	virtual void finishCreate();

	// Called from resizeGetHeight() when MTPDmessage_ClientFlag::f_pending_init_dimensions is set.
	virtual void initDimensions() = 0;

	virtual void markMediaAsReadHook() {
	}

	virtual int resizeContentGetHeight() = 0;

	void finishEdition(int oldKeyboardTop);
	void finishEditionToEmpty();

	const not_null<History*> _history;
	not_null<PeerData*> _from;
	HistoryBlock *_block = nullptr;
	int _indexInBlock = -1;
	MTPDmessage::Flags _flags = 0;

	HistoryItem *previousItem() const {
		if (_block && _indexInBlock >= 0) {
			if (_indexInBlock > 0) {
				return _block->items.at(_indexInBlock - 1);
			}
			if (auto previous = _block->previousBlock()) {
				Assert(!previous->items.empty());
				return previous->items.back();
			}
		}
		return nullptr;
	}
	HistoryItem *nextItem() const {
		if (_block && _indexInBlock >= 0) {
			if (_indexInBlock + 1 < _block->items.size()) {
				return _block->items.at(_indexInBlock + 1);
			}
			if (auto next = _block->nextBlock()) {
				Assert(!next->items.empty());
				return next->items.front();
			}
		}
		return nullptr;
	}

	// This should be called only from previousItemChanged()
	// to add required bits to the Composer mask
	// after that always use Has<HistoryMessageDate>().
	void recountDisplayDate();

	// This should be called only from previousItemChanged() or when
	// HistoryMessageDate or HistoryMessageUnreadBar bit is changed in the Composer mask
	// then the result should be cached in a client side flag MTPDmessage_ClientFlag::f_attach_to_previous.
	void recountAttachToPrevious();

	// This should be called only from recountDisplayDate().
	// Also this is called from setLogEntryDisplayDate() for channel log entries.
	void setDisplayDate(bool displayDate);

	// This should be called only from recountAttachToPrevious().
	// Also this is called from setLogEntryAttachToPrevious() for channel log entries.
	void setAttachToPrevious(bool attachToNext);

	// This should be called only from recountAttachToPrevious() of the next item
	// or when the next item is removed through nextItemChanged() call.
	// Also this is called from setLogEntryAttachToNext() for channel log entries.
	void setAttachToNext(bool attachToNext);

	const HistoryMessageReplyMarkup *inlineReplyMarkup() const {
		return const_cast<HistoryItem*>(this)->inlineReplyMarkup();
	}
	const ReplyKeyboard *inlineReplyKeyboard() const {
		return const_cast<HistoryItem*>(this)->inlineReplyKeyboard();
	}
	HistoryMessageReplyMarkup *inlineReplyMarkup() {
		if (auto markup = Get<HistoryMessageReplyMarkup>()) {
			if (markup->flags & MTPDreplyKeyboardMarkup_ClientFlag::f_inline) {
				return markup;
			}
		}
		return nullptr;
	}
	ReplyKeyboard *inlineReplyKeyboard() {
		if (auto markup = inlineReplyMarkup()) {
			return markup->inlineKeyboard.get();
		}
		return nullptr;
	}
	void invalidateChatsListEntry();

	[[nodiscard]] TextSelection skipTextSelection(
			TextSelection selection) const {
		return internal::unshiftSelection(selection, _text);
	}
	[[nodiscard]] TextSelection unskipTextSelection(
			TextSelection selection) const {
		return internal::shiftSelection(selection, _text);
	}

	Text _text = { int(st::msgMinWidth) };
	int _textWidth = -1;
	int _textHeight = 0;

	HistoryMediaPtr _media;

private:
	void resetGroupMedia(const std::vector<not_null<HistoryItem*>> &others);

	int _y = 0;
	int _width = 0;

};

// make all the constructors in HistoryItem children protected
// and wrapped with a static create() call with the same args
// so that history item can not be created directly, without
// calling a virtual finishCreate() method
template <typename T>
class HistoryItemInstantiated {
public:
	template <typename ...Args>
	static not_null<T*> _create(Args &&... args) {
		auto result = new T(std::forward<Args>(args)...);
		result->finishCreate();
		return result;
	}

};

ClickHandlerPtr goToMessageClickHandler(PeerData *peer, MsgId msgId);

inline ClickHandlerPtr goToMessageClickHandler(HistoryItem *item) {
	return goToMessageClickHandler(item->history()->peer, item->id);
}
