// This file is part of Desktop App Toolkit,
// a set of libraries for developing nice desktop applications.
//
// For license and copyright information please follow this link:
// https://github.com/desktop-app/legal/blob/master/LEGAL
//
#include "ui/widgets/inner_dropdown.h"

#include "ui/widgets/scroll_area.h"
#include "ui/widgets/shadow.h"
#include "ui/effects/panel_animation.h"
#include "ui/image/image_prepare.h"
#include "ui/qt_weak_factory.h"
#include "ui/ui_utility.h"

namespace Ui {

InnerDropdown::InnerDropdown(
	QWidget *parent,
	const style::InnerDropdown &st)
: RpWidget(parent)
, _st(st)
, _roundRect(ImageRoundRadius::Small, _st.bg)
, _hideTimer([=] { hideAnimated(); })
, _scroll(this, _st.scroll) {
	_scroll->scrolls(
	) | rpl::start_with_next([=] {
		scrolled();
	}, lifetime());

	hide();

	shownValue(
	) | rpl::filter([](bool shown) {
		return shown;
	}) | rpl::take(1) | rpl::map([=] {
		// We can't invoke this before the window is created.
		// So instead we start handling them on the first show().
		return macWindowDeactivateEvents();
	}) | rpl::flatten_latest(
	) | rpl::filter([=] {
		return !isHidden();
	}) | rpl::start_with_next([=] {
		leaveEvent(nullptr);
	}, lifetime());
}

QPointer<RpWidget> InnerDropdown::doSetOwnedWidget(
		object_ptr<RpWidget> widget) {
	auto result = QPointer<RpWidget>(widget);
	widget->heightValue(
	) | rpl::skip(1) | rpl::start_with_next([=] {
		resizeToContent();
	}, widget->lifetime());
	auto container = _scroll->setOwnedWidget(
		object_ptr<Container>(
			_scroll,
			std::move(widget),
			_st));
	container->resizeToWidth(_scroll->width());
	container->moveToLeft(0, 0);
	container->show();
	result->show();
	return result;
}

void InnerDropdown::setMaxHeight(int newMaxHeight) {
	_maxHeight = newMaxHeight;
	resizeToContent();
}

void InnerDropdown::resizeToContent() {
	auto newWidth = _st.padding.left() + _st.scrollMargin.left() + _st.scrollMargin.right() + _st.padding.right();
	auto newHeight = _st.padding.top() + _st.scrollMargin.top() + _st.scrollMargin.bottom() + _st.padding.bottom();
	if (auto widget = static_cast<Container*>(_scroll->widget())) {
		widget->resizeToContent();
		newWidth += widget->width();
		newHeight += widget->height();
	}
	if (_maxHeight > 0) {
		accumulate_min(newHeight, _maxHeight);
	}
	if (newWidth != width() || newHeight != height()) {
		resize(newWidth, newHeight);
		update();
		finishAnimating();
	}
}

void InnerDropdown::resizeEvent(QResizeEvent *e) {
	_scroll->setGeometry(rect().marginsRemoved(_st.padding).marginsRemoved(_st.scrollMargin));
	if (auto widget = static_cast<TWidget*>(_scroll->widget())) {
		widget->resizeToWidth(_scroll->width());
		scrolled();
	}
}

void InnerDropdown::scrolled() {
	if (auto widget = static_cast<TWidget*>(_scroll->widget())) {
		int visibleTop = _scroll->scrollTop();
		int visibleBottom = visibleTop + _scroll->height();
		widget->setVisibleTopBottom(visibleTop, visibleBottom);
	}
}

void InnerDropdown::paintEvent(QPaintEvent *e) {
	QPainter p(this);

	if (_a_show.animating()) {
		if (auto opacity = _a_opacity.value(_hiding ? 0. : 1.)) {
			// _a_opacity.current(ms)->opacityAnimationCallback()->_showAnimation.reset()
			if (_showAnimation) {
				_showAnimation->paintFrame(p, 0, 0, width(), _a_show.value(1.), opacity);
			}
		}
	} else if (_a_opacity.animating()) {
		p.setOpacity(_a_opacity.value(0.));
		p.drawPixmap(0, 0, _cache);
	} else if (_hiding || isHidden()) {
		hideFinished();
	} else if (_showAnimation) {
		_showAnimation->paintFrame(p, 0, 0, width(), 1., 1.);
		_showAnimation.reset();
		showChildren();
	} else {
		if (!_cache.isNull()) _cache = QPixmap();
		const auto inner = rect().marginsRemoved(_st.padding);
		Shadow::paint(p, inner, width(), _st.shadow);
		_roundRect.paint(p, inner);
	}
}

void InnerDropdown::enterEventHook(QEnterEvent *e) {
	if (_autoHiding) {
		showAnimated(_origin);
	}
	return RpWidget::enterEventHook(e);
}

void InnerDropdown::leaveEventHook(QEvent *e) {
	if (_autoHiding) {
		if (_a_show.animating() || _a_opacity.animating()) {
			hideAnimated();
		} else {
			_hideTimer.callOnce(300);
		}
	}
	return RpWidget::leaveEventHook(e);
}

void InnerDropdown::otherEnter() {
	if (_autoHiding) {
		showAnimated(_origin);
	}
}

void InnerDropdown::otherLeave() {
	if (_autoHiding) {
		if (_a_show.animating() || _a_opacity.animating()) {
			hideAnimated();
		} else {
			_hideTimer.callOnce(0);
		}
	}
}

void InnerDropdown::setOrigin(PanelAnimation::Origin origin) {
	_origin = origin;
}

void InnerDropdown::showAnimated(PanelAnimation::Origin origin) {
	setOrigin(origin);
	showAnimated();
}

void InnerDropdown::showAnimated() {
	_hideTimer.cancel();
	showStarted();
}

void InnerDropdown::hideAnimated(HideOption option) {
	if (isHidden()) return;
	if (option == HideOption::IgnoreShow) {
		_ignoreShowEvents = true;
	}
	if (_hiding) return;

	_hideTimer.cancel();
	startOpacityAnimation(true);
}

void InnerDropdown::finishAnimating() {
	if (_a_show.animating()) {
		_a_show.stop();
		showAnimationCallback();
	}
	if (_showAnimation) {
		_showAnimation.reset();
		showChildren();
	}
	if (_a_opacity.animating()) {
		_a_opacity.stop();
		opacityAnimationCallback();
	}
}

void InnerDropdown::showFast() {
	_hideTimer.cancel();
	finishAnimating();
	if (isHidden()) {
		showChildren();
		show();
	}
	_hiding = false;
}

void InnerDropdown::hideFast() {
	if (isHidden()) return;

	_hideTimer.cancel();
	finishAnimating();
	_hiding = false;
	hideFinished();
}

void InnerDropdown::hideFinished() {
	_a_show.stop();
	_showAnimation.reset();
	_cache = QPixmap();
	_ignoreShowEvents = false;
	if (!isHidden()) {
		const auto weak = Ui::MakeWeak(this);
		if (const auto onstack = _hiddenCallback) {
			onstack();
		}
		if (weak) {
			hide();
		}
	}
}

void InnerDropdown::prepareCache() {
	if (_a_opacity.animating()) return;

	const auto animating = _a_show.animating();
	auto showAnimation = base::take(_a_show);
	auto showAnimationData = base::take(_showAnimation);
	showChildren();
	_cache = GrabWidget(this);
	if (animating) {
		hideChildren();
	}
	_showAnimation = base::take(showAnimationData);
	_a_show = base::take(showAnimation);
}

void InnerDropdown::startOpacityAnimation(bool hiding) {
	const auto weak = Ui::MakeWeak(this);
	if (hiding) {
		if (const auto onstack = _hideStartCallback) {
			onstack();
		}
	} else if (const auto onstack = _showStartCallback) {
		onstack();
	}
	if (!weak) {
		return;
	}

	_hiding = false;
	prepareCache();
	_hiding = hiding;
	hideChildren();
	_a_opacity.start(
		[=] { opacityAnimationCallback(); },
		_hiding ? 1. : 0.,
		_hiding ? 0. : 1.,
		_st.duration);
}

void InnerDropdown::showStarted() {
	if (_ignoreShowEvents) return;
	if (isHidden()) {
		show();
		startShowAnimation();
		return;
	} else if (!_hiding) {
		return;
	}
	startOpacityAnimation(false);
}

void InnerDropdown::startShowAnimation() {
	if (_showStartCallback) {
		_showStartCallback();
	}
	if (!_a_show.animating()) {
		auto opacityAnimation = base::take(_a_opacity);
		showChildren();
		auto cache = grabForPanelAnimation();
		_a_opacity = base::take(opacityAnimation);

		const auto pixelRatio = style::DevicePixelRatio();
		_showAnimation = std::make_unique<PanelAnimation>(_st.animation, _origin);
		auto inner = rect().marginsRemoved(_st.padding);
		_showAnimation->setFinalImage(std::move(cache), QRect(inner.topLeft() * pixelRatio, inner.size() * pixelRatio));
		_showAnimation->setCornerMasks(
			Images::CornersMask(ImageRoundRadius::Small));
		_showAnimation->start();
	}
	hideChildren();
	_a_show.start([this] { showAnimationCallback(); }, 0., 1., _st.showDuration);
}

QImage InnerDropdown::grabForPanelAnimation() {
	SendPendingMoveResizeEvents(this);
	const auto pixelRatio = style::DevicePixelRatio();
	auto result = QImage(size() * pixelRatio, QImage::Format_ARGB32_Premultiplied);
	result.setDevicePixelRatio(pixelRatio);
	result.fill(Qt::transparent);
	{
		QPainter p(&result);
		_roundRect.paint(p, rect().marginsRemoved(_st.padding));
		for (const auto child : children()) {
			if (const auto widget = qobject_cast<QWidget*>(child)) {
				RenderWidget(p, widget, widget->pos());
			}
		}
	}
	return result;
}

void InnerDropdown::opacityAnimationCallback() {
	update();
	if (!_a_opacity.animating()) {
		if (_hiding) {
			_hiding = false;
			hideFinished();
		} else if (!_a_show.animating()) {
			showChildren();
		}
	}
}

void InnerDropdown::showAnimationCallback() {
	update();
}

bool InnerDropdown::eventFilter(QObject *obj, QEvent *e) {
	if (e->type() == QEvent::Enter) {
		otherEnter();
	} else if (e->type() == QEvent::Leave) {
		otherLeave();
	} else if (e->type() == QEvent::MouseButtonRelease && static_cast<QMouseEvent*>(e)->button() == Qt::LeftButton) {
		if (isHidden() || _hiding) {
			otherEnter();
		} else {
			otherLeave();
		}
	}
	return false;
}

int InnerDropdown::resizeGetHeight(int newWidth) {
	auto newHeight = _st.padding.top() + _st.scrollMargin.top() + _st.scrollMargin.bottom() + _st.padding.bottom();
	if (auto widget = static_cast<TWidget*>(_scroll->widget())) {
		auto containerWidth = newWidth - _st.padding.left() - _st.padding.right() - _st.scrollMargin.left() - _st.scrollMargin.right();
		widget->resizeToWidth(containerWidth);
		newHeight += widget->height();
	}
	if (_maxHeight > 0) {
		accumulate_min(newHeight, _maxHeight);
	}
	return newHeight;
}

InnerDropdown::Container::Container(QWidget *parent, object_ptr<TWidget> child, const style::InnerDropdown &st) : TWidget(parent)
, _child(std::move(child))
, _st(st) {
	_child->setParent(this);
	_child->moveToLeft(_st.scrollPadding.left(), _st.scrollPadding.top());
}

void InnerDropdown::Container::visibleTopBottomUpdated(
		int visibleTop,
		int visibleBottom) {
	setChildVisibleTopBottom(_child, visibleTop, visibleBottom);
}

void InnerDropdown::Container::resizeToContent() {
	auto newWidth = _st.scrollPadding.left() + _st.scrollPadding.right();
	auto newHeight = _st.scrollPadding.top() + _st.scrollPadding.bottom();
	if (auto child = static_cast<TWidget*>(children().front())) {
		newWidth += child->width();
		newHeight += child->height();
	}
	if (newWidth != width() || newHeight != height()) {
		resize(newWidth, newHeight);
	}
}

int InnerDropdown::Container::resizeGetHeight(int newWidth) {
	auto innerWidth = newWidth - _st.scrollPadding.left() - _st.scrollPadding.right();
	auto result = _st.scrollPadding.top() + _st.scrollPadding.bottom();
	_child->resizeToWidth(innerWidth);
	_child->moveToLeft(_st.scrollPadding.left(), _st.scrollPadding.top());
	result += _child->height();
	return result;
}

} // namespace Ui
