#include "macro-action-scene-transform.hpp"
#include "utility.hpp"

#include <graphics/matrix4.h>

namespace advss {

const std::string MacroActionSceneTransform::id = "scene_transform";

bool MacroActionSceneTransform::_registered = MacroActionFactory::Register(
	MacroActionSceneTransform::id,
	{MacroActionSceneTransform::Create,
	 MacroActionSceneTransformEdit::Create,
	 "AdvSceneSwitcher.action.sceneTransform"});

const static std::map<MacroActionSceneTransform::Action, std::string> actions = {
	{MacroActionSceneTransform::Action::MANUAL_TRANSFORM,
	 "AdvSceneSwitcher.action.sceneTransform.type.manual"},
	{MacroActionSceneTransform::Action::RESET,
	 "AdvSceneSwitcher.action.sceneTransform.type.reset"},
	{MacroActionSceneTransform::Action::ROTATE,
	 "AdvSceneSwitcher.action.sceneTransform.type.rotate"},
	{MacroActionSceneTransform::Action::FLIP_HORIZONTAL,
	 "AdvSceneSwitcher.action.sceneTransform.type.flipHorizontal"},
	{MacroActionSceneTransform::Action::FLIP_VERTICAL,
	 "AdvSceneSwitcher.action.sceneTransform.type.flipVertical"},
	{MacroActionSceneTransform::Action::FIT_TO_SCREEN,
	 "AdvSceneSwitcher.action.sceneTransform.type.fitToScreen"},
	{MacroActionSceneTransform::Action::STRETCH_TO_SCREEN,
	 "AdvSceneSwitcher.action.sceneTransform.type.stretchToScreen"},
	{MacroActionSceneTransform::Action::CENTER_TO_SCREEN,
	 "AdvSceneSwitcher.action.sceneTransform.type.centerToScreen"},
	{MacroActionSceneTransform::Action::CENTER_VERTICALLY,
	 "AdvSceneSwitcher.action.sceneTransform.type.centerVertically"},
	{MacroActionSceneTransform::Action::CENTER_HORIZONTALLY,
	 "AdvSceneSwitcher.action.sceneTransform.type.centerHorizontally"},
};

/* --------------- Helpers based on OBS window-basic-main.cpp --------------- */

static void getItemBox(obs_sceneitem_t *item, vec3 &tl, vec3 &br)
{
	matrix4 boxTransform;
	obs_sceneitem_get_box_transform(item, &boxTransform);

	vec3_set(&tl, M_INFINITE, M_INFINITE, 0.0f);
	vec3_set(&br, -M_INFINITE, -M_INFINITE, 0.0f);

	auto GetMinPos = [&](float x, float y) {
		vec3 pos;
		vec3_set(&pos, x, y, 0.0f);
		vec3_transform(&pos, &pos, &boxTransform);
		vec3_min(&tl, &tl, &pos);
		vec3_max(&br, &br, &pos);
	};

	GetMinPos(0.0f, 0.0f);
	GetMinPos(1.0f, 0.0f);
	GetMinPos(0.0f, 1.0f);
	GetMinPos(1.0f, 1.0f);
}

static vec3 getItemTL(obs_sceneitem_t *item)
{
	vec3 tl, br;
	getItemBox(item, tl, br);
	return tl;
}

static void setItemTL(obs_sceneitem_t *item, const vec3 &tl)
{
	vec3 newTL;
	vec2 pos;

	obs_sceneitem_get_pos(item, &pos);
	newTL = getItemTL(item);
	pos.x += tl.x - newTL.x;
	pos.y += tl.y - newTL.y;
	obs_sceneitem_set_pos(item, &pos);
}

static void reset(obs_sceneitem_t *item)
{
	obs_transform_info info;
	vec2_set(&info.pos, 0.0f, 0.0f);
	vec2_set(&info.scale, 1.0f, 1.0f);
	info.rot = 0.0f;
	info.alignment = OBS_ALIGN_TOP | OBS_ALIGN_LEFT;
	info.bounds_type = OBS_BOUNDS_NONE;
	info.bounds_alignment = OBS_ALIGN_CENTER;
	vec2_set(&info.bounds, 0.0f, 0.0f);
	obs_sceneitem_set_info(item, &info);

	obs_sceneitem_crop crop = {};
	obs_sceneitem_set_crop(item, &crop);
}

static void rotate(obs_sceneitem_t *item, float rot)
{
	vec3 tl = getItemTL(item);
	rot += obs_sceneitem_get_rot(item);
	if (rot >= 360.0f) {
		rot -= 360.0f;
	} else if (rot <= -360.0f) {
		rot += 360.0f;
	}
	obs_sceneitem_set_rot(item, rot);
	obs_sceneitem_force_update_transform(item);
	setItemTL(item, tl);
};

static void flip(obs_sceneitem_t *item, const vec2 &mul)
{
	vec3 tl = getItemTL(item);
	vec2 scale;
	obs_sceneitem_get_scale(item, &scale);
	vec2_mul(&scale, &scale, &mul);
	obs_sceneitem_set_scale(item, &scale);
	obs_sceneitem_force_update_transform(item);
	setItemTL(item, tl);
}

static void centerAlign(obs_sceneitem_t *item, obs_bounds_type boundsType)
{
	obs_video_info ovi;
	obs_get_video_info(&ovi);

	obs_transform_info itemInfo;
	vec2_set(&itemInfo.pos, 0.0f, 0.0f);
	vec2_set(&itemInfo.scale, 1.0f, 1.0f);
	itemInfo.alignment = OBS_ALIGN_LEFT | OBS_ALIGN_TOP;
	itemInfo.rot = 0.0f;

	vec2_set(&itemInfo.bounds, float(ovi.base_width),
		 float(ovi.base_height));
	itemInfo.bounds_type = boundsType;
	itemInfo.bounds_alignment = OBS_ALIGN_CENTER;

	obs_sceneitem_set_info(item, &itemInfo);
}

enum class CenterType {
	SCREEN,
	VERTICAL,
	HORIZONTAL,
};

static void center(obs_sceneitem_t *item, const CenterType &centerType)
{
	// Get center x, y coordinates of items
	vec3 tl, br;
	getItemBox(item, tl, br);

	float left = tl.x;
	float top = tl.y;
	float right = br.x;
	float bottom = br.y;

	vec3 center;
	center.x = (right + left) / 2.0f;
	center.y = (top + bottom) / 2.0f;
	center.z = 0.0f;

	// Get coordinates of screen center
	obs_video_info ovi;
	obs_get_video_info(&ovi);

	vec3 screenCenter;
	vec3_set(&screenCenter, float(ovi.base_width), float(ovi.base_height),
		 0.0f);

	vec3_mulf(&screenCenter, &screenCenter, 0.5f);

	// Calculate difference between screen center and item center
	vec3 offset;
	vec3_sub(&offset, &screenCenter, &center);

	// Shift item by offset
	vec3_add(&tl, &tl, &offset);

	vec3 itemTL = getItemTL(item);

	if (centerType == CenterType::VERTICAL) {
		tl.x = itemTL.x;
	} else if (centerType == CenterType::HORIZONTAL) {
		tl.y = itemTL.y;
	}

	setItemTL(item, tl);
}

/* ------------------------------------------------------------------------- */

void MacroActionSceneTransform::Transform(obs_scene_item *item)
{
	if (!item) {
		return;
	}

	switch (_action) {
	case MacroActionSceneTransform::Action::RESET:
		obs_sceneitem_defer_update_begin(item);
		reset(item);
		obs_sceneitem_defer_update_end(item);

		break;
	case MacroActionSceneTransform::Action::ROTATE:
		rotate(item, _rotation);
		break;
	case MacroActionSceneTransform::Action::FLIP_HORIZONTAL: {
		vec2 scale;
		vec2_set(&scale, -1.0f, 1.0f);
		flip(item, scale);
		break;
	}
	case MacroActionSceneTransform::Action::FLIP_VERTICAL: {
		vec2 scale;
		vec2_set(&scale, 1.0f, -1.0f);
		flip(item, scale);
		break;
	}
	case MacroActionSceneTransform::Action::FIT_TO_SCREEN:
		centerAlign(item, OBS_BOUNDS_SCALE_INNER);
		break;
	case MacroActionSceneTransform::Action::STRETCH_TO_SCREEN:
		centerAlign(item, OBS_BOUNDS_STRETCH);
		break;
	case MacroActionSceneTransform::Action::CENTER_TO_SCREEN:
		center(item, CenterType::SCREEN);
		break;
	case MacroActionSceneTransform::Action::CENTER_VERTICALLY:
		center(item, CenterType::VERTICAL);
		break;
	case MacroActionSceneTransform::Action::CENTER_HORIZONTALLY:
		center(item, CenterType::HORIZONTAL);
		break;
	case MacroActionSceneTransform::Action::MANUAL_TRANSFORM:
		ApplySettings(_settings); // Resolves variables
		obs_sceneitem_defer_update_begin(item);
		obs_sceneitem_set_info(item, &_info);
		obs_sceneitem_set_crop(item, &_crop);
		obs_sceneitem_defer_update_end(item);
		break;
	}
}

bool MacroActionSceneTransform::PerformAction()
{
	auto items = _source.GetSceneItems(_scene);
	for (const auto &item : items) {
		Transform(item);
	}
	return true;
}

void MacroActionSceneTransform::LogAction() const
{
	vblog(LOG_INFO,
	      "performed transform action %d for source \"%s\" on scene \"%s\"",
	      static_cast<int>(_action), _source.ToString(true).c_str(),
	      _scene.ToString(true).c_str());
}

bool MacroActionSceneTransform::Save(obs_data_t *obj) const
{
	MacroAction::Save(obj);
	obs_data_set_int(obj, "action", static_cast<int>(_action));
	_scene.Save(obj);
	_source.Save(obj);
	_rotation.Save(obj, "rotation");
	_settings.Save(obj, "settings");
	return true;
}

bool MacroActionSceneTransform::Load(obs_data_t *obj)
{
	// Convert old data format
	// TODO: Remove in future version
	if (obs_data_has_user_value(obj, "source")) {
		auto sourceName = obs_data_get_string(obj, "source");
		obs_data_set_string(obj, "sceneItem", sourceName);
	}

	MacroAction::Load(obj);
	if (!obs_data_has_user_value(obj, "action")) {
		_action =
			Action::MANUAL_TRANSFORM; // TODO: Remove fallback in future
	} else {
		_action = static_cast<Action>(obs_data_get_int(obj, "action"));
	}
	_scene.Load(obj);
	_source.Load(obj);
	_rotation.Load(obj, "rotation");
	_settings.Load(obj, "settings");

	// Convert old data format
	// TODO: Remove in future version
	if (!obs_data_has_user_value(obj, "settings")) {
		LoadTransformState(obj, _info, _crop);
		_settings = ConvertSettings();
	}
	return true;
}

std::string MacroActionSceneTransform::GetShortDesc() const
{
	if (_source.ToString().empty()) {
		return "";
	}
	return _scene.ToString() + " - " + _source.ToString();
}

std::string MacroActionSceneTransform::ConvertSettings()
{
	auto data = obs_data_create();
	SaveTransformState(data, _info, _crop);
	std::string json = obs_data_get_json(data);
	obs_data_release(data);
	return json;
}

void MacroActionSceneTransform::ApplySettings(const std::string &settings)
{
	auto data = obs_data_create_from_json(settings.c_str());
	if (!data) {
		return;
	}
	LoadTransformState(data, _info, _crop);
	auto items = _source.GetSceneItems(_scene);
	if (items.empty()) {
		return;
	}
	if (obs_data_has_user_value(data, "size")) {
		auto obj = obs_data_get_obj(data, "size");
		auto source = obs_sceneitem_get_source(items[0]);
		if (double h = obs_data_get_double(obj, "height")) {
			_info.scale.y =
				h / double(obs_source_get_height(source));
		}
		if (double w = obs_data_get_double(obj, "width")) {
			_info.scale.x =
				w / double(obs_source_get_width(source));
		}
		obs_data_release(obj);
	}
	obs_data_release(data);
}

static inline void populateActionSelection(QComboBox *list)
{
	for (const auto &[type, name] : actions) {
		list->addItem(obs_module_text(name.c_str()),
			      static_cast<int>(type));
	}
}

MacroActionSceneTransformEdit::MacroActionSceneTransformEdit(
	QWidget *parent, std::shared_ptr<MacroActionSceneTransform> entryData)
	: QWidget(parent),
	  _scenes(new SceneSelectionWidget(window(), true, false, false, true)),
	  _sources(new SceneItemSelectionWidget(parent)),
	  _action(new QComboBox()),
	  _rotation(new VariableDoubleSpinBox()),
	  _getSettings(new QPushButton(obs_module_text(
		  "AdvSceneSwitcher.action.sceneTransform.getTransform"))),
	  _settings(new VariableTextEdit(this)),
	  _buttonLayout(new QHBoxLayout())
{
	_rotation->setMinimum(-360);
	_rotation->setMaximum(360);
	_rotation->setSuffix("°");

	populateActionSelection(_action);

	QWidget::connect(_scenes, SIGNAL(SceneChanged(const SceneSelection &)),
			 this, SLOT(SceneChanged(const SceneSelection &)));
	QWidget::connect(_scenes, SIGNAL(SceneChanged(const SceneSelection &)),
			 _sources, SLOT(SceneChanged(const SceneSelection &)));
	QWidget::connect(_sources,
			 SIGNAL(SceneItemChanged(const SceneItemSelection &)),
			 this, SLOT(SourceChanged(const SceneItemSelection &)));
	QWidget::connect(_action, SIGNAL(currentIndexChanged(int)), this,
			 SLOT(ActionChanged(int)));
	QWidget::connect(
		_rotation,
		SIGNAL(NumberVariableChanged(const NumberVariable<double> &)),
		this, SLOT(RotationChanged(const NumberVariable<double> &)));
	QWidget::connect(_getSettings, SIGNAL(clicked()), this,
			 SLOT(GetSettingsClicked()));
	QWidget::connect(_settings, SIGNAL(textChanged()), this,
			 SLOT(SettingsChanged()));

	QHBoxLayout *entryLayout = new QHBoxLayout;

	std::unordered_map<std::string, QWidget *> widgetPlaceholders = {
		{"{{scenes}}", _scenes},     {"{{rotation}}", _rotation},
		{"{{sources}}", _sources},   {"{{action}}", _action},
		{"{{settings}}", _settings}, {"{{getSettings}}", _getSettings},
	};
	PlaceWidgets(
		obs_module_text("AdvSceneSwitcher.action.sceneTransform.entry"),
		entryLayout, widgetPlaceholders);

	_buttonLayout->addWidget(_getSettings);
	_buttonLayout->addStretch();

	QVBoxLayout *mainLayout = new QVBoxLayout;
	mainLayout->addLayout(entryLayout);
	mainLayout->addWidget(_settings);
	mainLayout->addLayout(_buttonLayout);
	setLayout(mainLayout);

	_entryData = entryData;
	UpdateEntryData();
	_loading = false;
}

void MacroActionSceneTransformEdit::UpdateEntryData()
{
	if (!_entryData) {
		return;
	}

	_scenes->SetScene(_entryData->_scene);
	_sources->SetSceneItem(_entryData->_source);
	_action->setCurrentIndex(
		_action->findData(static_cast<int>(_entryData->_action)));
	_rotation->SetValue(_entryData->_rotation);
	_settings->setPlainText(_entryData->_settings);
	SetWidgetVisibility();
}

void MacroActionSceneTransformEdit::SceneChanged(const SceneSelection &s)
{
	if (_loading || !_entryData) {
		return;
	}

	auto lock = LockContext();
	_entryData->_scene = s;
}

void MacroActionSceneTransformEdit::SourceChanged(const SceneItemSelection &item)
{
	if (_loading || !_entryData) {
		return;
	}

	auto lock = LockContext();
	_entryData->_source = item;
	emit HeaderInfoChanged(
		QString::fromStdString(_entryData->GetShortDesc()));
	adjustSize();
	updateGeometry();
}

void MacroActionSceneTransformEdit::ActionChanged(int idx)
{
	if (_loading || !_entryData) {
		return;
	}

	auto lock = LockContext();
	_entryData->_action = static_cast<MacroActionSceneTransform::Action>(
		_action->itemData(idx).toInt());
	SetWidgetVisibility();
}

void MacroActionSceneTransformEdit::RotationChanged(const DoubleVariable &value)
{
	if (_loading || !_entryData) {
		return;
	}

	auto lock = LockContext();
	_entryData->_rotation = value;
}

void MacroActionSceneTransformEdit::GetSettingsClicked()
{
	if (_loading || !_entryData || !_entryData->_scene.GetScene(false)) {
		return;
	}

	auto items = _entryData->_source.GetSceneItems(_entryData->_scene);
	if (items.empty()) {
		return;
	}

	auto settings = GetSceneItemTransform(items[0]);
	_settings->setPlainText(FormatJsonString(settings));
}

void MacroActionSceneTransformEdit::SettingsChanged()
{
	if (_loading || !_entryData) {
		return;
	}

	auto lock = LockContext();
	_entryData->_settings = _settings->toPlainText().toStdString();

	adjustSize();
	updateGeometry();
}

void MacroActionSceneTransformEdit::SetWidgetVisibility()
{
	SetLayoutVisible(
		_buttonLayout,
		_entryData->_action ==
			MacroActionSceneTransform::Action::MANUAL_TRANSFORM);
	_settings->setVisible(
		_entryData->_action ==
		MacroActionSceneTransform::Action::MANUAL_TRANSFORM);
	_rotation->setVisible(_entryData->_action ==
			      MacroActionSceneTransform::Action::ROTATE);
	adjustSize();
	updateGeometry();
}

} // namespace advss
