/*
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 "editor/color_picker.h"

#include "ui/painter.h"
#include "ui/rp_widget.h"
#include "styles/style_editor.h"

#include <QtGui/QLinearGradient>

namespace Editor {
namespace {

constexpr auto kPrecision = 1000;
constexpr auto kMinBrushSize = 0.1f;
constexpr auto kMouseSkip = 1.4;

constexpr auto kMinInnerHeight = 0.2;
constexpr auto kMaxInnerHeight = 0.8;

constexpr auto kCircleDuration = crl::time(200);

constexpr auto kMax = 1.0;

ColorPicker::OutlinedStop FindOutlinedStop(
		const QColor &color,
		const QGradientStops &stops,
		int width) {
	for (auto i = 0; i < stops.size(); i++) {
		const auto &current = stops[i];
		if (current.second == color) {
			const auto prev = ((i - 1) < 0)
				? std::nullopt
				: std::make_optional<int>(stops[i - 1].first * width);
			const auto next = ((i + 1) >= stops.size())
				? std::nullopt
				: std::make_optional<int>(stops[i + 1].first * width);
			return ColorPicker::OutlinedStop{
				.stopPos = (current.first * width),
				.prevStopPos = prev,
				.nextStopPos = next,
			};
		}
	}
	return ColorPicker::OutlinedStop();
}

QGradientStops Colors() {
	return QGradientStops{
		{ 0.00f, QColor(234, 39, 57) },
		{ 0.14f, QColor(219, 58, 210) },
		{ 0.24f, QColor(48, 81, 227) },
		{ 0.39f, QColor(73, 197, 237) },
		{ 0.49f, QColor(128, 200, 100) },
		{ 0.62f, QColor(252, 222, 101) },
		{ 0.73f, QColor(252, 150, 77) },
		{ 0.85f, QColor(0, 0, 0) },
		{ 1.00f, QColor(255, 255, 255) } };
}

QBrush GradientBrush(const QPoint &p, const QGradientStops &stops) {
	auto gradient = QLinearGradient(0, p.y(), p.x(), p.y());
	gradient.setStops(stops);
	return QBrush(std::move(gradient));
}

float RatioPrecise(float a) {
	return int(a * kPrecision) / float(kPrecision);
}

inline float64 InterpolateF(float a, float b, float64 b_ratio) {
	return a + float64(b - a) * b_ratio;
};

inline float64 InterpolationRatio(int from, int to, int result) {
	return (result - from) / float64(to - from);
};

} // namespace

ColorPicker::ColorPicker(
	not_null<Ui::RpWidget*> parent,
	const Brush &savedBrush)
: _circleColor(Qt::white)
, _width(st::photoEditorColorPickerWidth)
, _lineHeight(st::photoEditorColorPickerLineHeight)
, _colorLine(base::make_unique_q<Ui::RpWidget>(parent))
, _canvasForCircle(base::make_unique_q<Ui::RpWidget>(parent))
, _gradientStops(Colors())
, _outlinedStop(FindOutlinedStop(_circleColor, _gradientStops, _width))
, _gradientBrush(
	GradientBrush(QPoint(_width, _lineHeight / 2), _gradientStops))
, _brush(Brush{
	.sizeRatio = (savedBrush.sizeRatio
		? savedBrush.sizeRatio
		: kMinBrushSize),
	.color = (savedBrush.color.isValid()
		? savedBrush.color
		: _gradientStops.front().second),
}) {
	_colorLine->resize(_width, _lineHeight);
	_canvasForCircle->resize(
		_width + circleHeight(kMax),
		st::photoEditorColorPickerCanvasHeight);

	_canvasForCircle->setAttribute(Qt::WA_TransparentForMouseEvents);

	_down.pos = QPoint(colorToPosition(savedBrush.color), 0);

	_colorLine->paintRequest(
	) | rpl::start_with_next([=] {
		auto p = QPainter(_colorLine);
		PainterHighQualityEnabler hq(p);

		p.setPen(Qt::NoPen);
		p.setBrush(_gradientBrush);

		const auto radius = _colorLine->height() / 2.;
		p.drawRoundedRect(_colorLine->rect(), radius, radius);
	}, _colorLine->lifetime());

	_canvasForCircle->paintRequest(
	) | rpl::start_with_next([=] {
		auto p = QPainter(_canvasForCircle);
		paintCircle(p);
	}, _canvasForCircle->lifetime());

	_colorLine->events(
	) | rpl::start_with_next([=](not_null<QEvent*> event) {
		const auto type = event->type();
		const auto isPress = (type == QEvent::MouseButtonPress)
			|| (type == QEvent::MouseButtonDblClick);
		const auto isMove = (type == QEvent::MouseMove);
		const auto isRelease = (type == QEvent::MouseButtonRelease);
		if (!isPress && !isMove && !isRelease) {
			return;
		}
		_down.pressed = !isRelease;

		const auto progress = _circleAnimation.value(isPress ? 0. : 1.);
		if (!isMove) {
			const auto from = progress;
			const auto to = isPress ? 1. : 0.;
			_circleAnimation.stop();

			_circleAnimation.start(
				[=] { _canvasForCircle->update(); },
				from,
				to,
				kCircleDuration * std::abs(to - from),
				anim::easeOutCirc);
		}
		const auto e = static_cast<QMouseEvent*>(event.get());
		updateMousePosition(e->pos(), progress);
		if (isRelease) {
			_saveBrushRequests.fire_copy(_brush);
		}

		_canvasForCircle->update();
	}, _colorLine->lifetime());
}

void ColorPicker::updateMousePosition(const QPoint &pos, float64 progress) {
	const auto mapped = _canvasForCircle->mapFromParent(
		_colorLine->mapToParent(pos));

	const auto height = circleHeight(progress);
	const auto mappedY = int(mapped.y() - height * kMouseSkip);
	const auto bottom = _canvasForCircle->height() - circleHeight(kMax);
	const auto &skip = st::photoEditorColorPickerCircleSkip;

	_down.pos = QPoint(
		std::clamp(pos.x(), 0, _width),
		std::clamp(mappedY, 0, bottom - skip));

	// Convert Y to the brush size.
	const auto from = 0;
	const auto to = bottom - skip;

	// Don't change the brush size when we are on the color line.
	if (mappedY <= to) {
		_brush.sizeRatio = std::clamp(
			float(1. - InterpolationRatio(from, to, _down.pos.y())),
			kMinBrushSize,
			1.f);
	}
	_brush.color = positionToColor(_down.pos.x());
}

void ColorPicker::moveLine(const QPoint &position) {
	_colorLine->move(position
		- QPoint(_colorLine->width() / 2, _colorLine->height() / 2));

	_canvasForCircle->move(
		_colorLine->x() - circleHeight(kMax) / 2,
		_colorLine->y()
			+ _colorLine->height()
			+ ((circleHeight() - _colorLine->height()) / 2)
			- _canvasForCircle->height());
}

QColor ColorPicker::positionToColor(int x) const {
	const auto from = 0;
	const auto to = _width;
	const auto gradientRatio = InterpolationRatio(from, to, x);

	for (auto i = 1; i < _gradientStops.size(); i++) {
		const auto &previous = _gradientStops[i - 1];
		const auto &current = _gradientStops[i];
		const auto &fromStop = previous.first;
		const auto &toStop = current.first;
		const auto &fromColor = previous.second;
		const auto &toColor = current.second;

		if ((fromStop <= gradientRatio) && (toStop >= gradientRatio)) {
			const auto stopRatio = RatioPrecise(
				(gradientRatio - fromStop) / float64(toStop - fromStop));
			return anim::color(fromColor, toColor, stopRatio);
		}
	}
	return QColor();
}

void ColorPicker::paintCircle(QPainter &p) {
	PainterHighQualityEnabler hq(p);

	p.setPen(Qt::NoPen);
	p.setBrush(_circleColor);

	const auto progress = _circleAnimation.value(_down.pressed ? 1. : 0.);
	const auto h = circleHeight(progress);
	const auto bottom = _canvasForCircle->height() - h;

	const auto circleX = _down.pos.x() + (circleHeight(kMax) - h) / 2;
	const auto circleY = _circleAnimation.animating()
		? anim::interpolate(bottom, _down.pos.y(), progress)
		: _down.pressed
		? _down.pos.y()
		: bottom;

	const auto r = QRect(circleX, circleY, h, h);
	p.drawEllipse(r);

	const auto innerH = InterpolateF(
		h * kMinInnerHeight,
		h * kMaxInnerHeight,
		_brush.sizeRatio);

	p.setBrush(_brush.color);

	const auto innerRect = QRectF(
		r.x() + (r.width() - innerH) / 2.,
		r.y() + (r.height() - innerH) / 2.,
		innerH,
		innerH);

	paintOutline(p, innerRect);
	p.drawEllipse(innerRect);
}

void ColorPicker::paintOutline(QPainter &p, const QRectF &rect) {
	const auto &s = _outlinedStop;
	if (!s.stopPos) {
		return;
	}
	const auto draw = [&](float opacity) {
		p.save();
		p.setOpacity(opacity);
		p.setPen(Qt::lightGray);
		p.setPen(Qt::NoBrush);
		p.drawEllipse(rect);
		p.restore();
	};
	const auto x = _down.pos.x();
	if (s.prevStopPos && (x >= s.prevStopPos && x <= s.stopPos)) {
		const auto from = *s.prevStopPos;
		const auto to = *s.stopPos;
		const auto ratio = InterpolationRatio(from, to, x);
		if (ratio >= 0. && ratio <= 1.) {
			draw(ratio);
		}
	} else if (s.nextStopPos && (x >= s.stopPos && x <= s.nextStopPos)) {
		const auto from = *s.stopPos;
		const auto to = *s.nextStopPos;
		const auto ratio = InterpolationRatio(from, to, x);
		if (ratio >= 0. && ratio <= 1.) {
			draw(1. - ratio);
		}
	}
}

int ColorPicker::circleHeight(float64 progress) const {
	return anim::interpolate(
		st::photoEditorColorPickerCircleSize,
		st::photoEditorColorPickerCircleBigSize,
		progress);
}

void ColorPicker::setVisible(bool visible) {
	_colorLine->setVisible(visible);
	_canvasForCircle->setVisible(visible);
}

rpl::producer<Brush> ColorPicker::saveBrushRequests() const {
	return _saveBrushRequests.events_starting_with_copy(_brush);
}

int ColorPicker::colorToPosition(const QColor &color) const {
	const auto step = 1. / kPrecision;
	for (auto i = 0.; i <= 1.; i += step) {
		if (positionToColor(i * _width) == color) {
			return i * _width;
		}
	}
	return 0;
}

bool ColorPicker::preventHandleKeyPress() const {
	return _canvasForCircle->isVisible()
		&& (_circleAnimation.animating() || _down.pressed);
}

} // namespace Editor
