/*
obs-websocket
Copyright (C) 2016-2021 Stephane Lepin <stephane.lepin@gmail.com>
Copyright (C) 2020-2021 Kyle Manning <tt2468@gmail.com>

This program 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 2 of the License, or
(at your option) any later version.

This program 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.

You should have received a copy of the GNU General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>
*/

#include "RequestHandler.h"

/**
 * Gets an array of scenes in OBS.
 *
 * @requestField ?canvasUuid | String | UUID of the canvas the scenes are in
 * 
 * @responseField currentProgramSceneName | String        | Current program scene name. Can be `null` if non-main canvas or internal state desync
 * @responseField currentProgramSceneUuid | String        | Current program scene UUID. Can be `null` if non-main canvas or internal state desync
 * @responseField currentPreviewSceneName | String        | Current preview scene name. `null` if not in studio mode or non-main canvas
 * @responseField currentPreviewSceneUuid | String        | Current preview scene UUID. `null` if not in studio mode or non-main canvas
 * @responseField scenes                  | Array<Object> | Array of scenes
 *
 * @requestType GetSceneList
 * @complexity 2
 * @rpcVersion -1
 * @initialVersion 5.0.0
 * @api requests
 * @category scenes
 */
RequestResult RequestHandler::GetSceneList(const Request &request)
{
	RequestStatus::RequestStatus statusCode;
	std::string comment;
	OBSCanvasAutoRelease canvas = request.AcquireCanvas("canvasUuid", statusCode, comment);
	if (!canvas)
		return RequestResult::Error(statusCode, comment);

	json responseData;

	if (obs_canvas_get_flags(canvas) & MAIN) { // Main canvas, which has a program scene, preview scene, and scene order
		OBSSourceAutoRelease currentProgramScene = obs_frontend_get_current_scene();
		if (currentProgramScene) {
			responseData["currentProgramSceneName"] = obs_source_get_name(currentProgramScene);
			responseData["currentProgramSceneUuid"] = obs_source_get_uuid(currentProgramScene);
		} else {
			responseData["currentProgramSceneName"] = nullptr;
			responseData["currentProgramSceneUuid"] = nullptr;
		}

		OBSSourceAutoRelease currentPreviewScene = obs_frontend_get_current_preview_scene();
		if (currentPreviewScene) {
			responseData["currentPreviewSceneName"] = obs_source_get_name(currentPreviewScene);
			responseData["currentPreviewSceneUuid"] = obs_source_get_uuid(currentPreviewScene);
		} else {
			responseData["currentPreviewSceneName"] = nullptr;
			responseData["currentPreviewSceneUuid"] = nullptr;
		}
		responseData["scenes"] = Utils::Obs::ArrayHelper::GetSceneList();
	} else { // Non-main canvas, which only has an unordered array of scenes
		responseData["currentProgramSceneName"] = nullptr;
		responseData["currentProgramSceneUuid"] = nullptr;
		responseData["currentProgramSceneName"] = nullptr;
		responseData["currentProgramSceneUuid"] = nullptr;
		responseData["scenes"] = Utils::Obs::ArrayHelper::GetCanvasSceneList(canvas);
	}

	return RequestResult::Success(responseData);
}

/**
 * Gets an array of all groups in OBS.
 *
 * Groups in OBS are actually scenes, but renamed and modified. In obs-websocket, we treat them as scenes where we can.
 *
 * @responseField groups | Array<String> | Array of group names
 *
 * @requestType GetGroupList
 * @complexity 2
 * @rpcVersion -1
 * @initialVersion 5.0.0
 * @api requests
 * @category scenes
 */
RequestResult RequestHandler::GetGroupList(const Request &request)
{
	RequestStatus::RequestStatus statusCode;
	std::string comment;
	OBSCanvasAutoRelease canvas = request.AcquireCanvas("canvasUuid", statusCode, comment);
	if (!canvas)
		return RequestResult::Error(statusCode, comment);

	json responseData;
	responseData["groups"] = Utils::Obs::ArrayHelper::GetCanvasGroupList(canvas);

	return RequestResult::Success(responseData);
}

/**
 * Gets the current program scene.
 *
 * Note 1: This request is slated to have the `currentProgram`-prefixed fields removed from in an upcoming RPC version.
 *
 * Note 2: Canvases do not have any concept of a program or preview scene, so this request does not support canvases.
 *
 * @responseField sceneName               | String | Current program scene name
 * @responseField sceneUuid               | String | Current program scene UUID
 * @responseField currentProgramSceneName | String | Current program scene name (Deprecated)
 * @responseField currentProgramSceneUuid | String | Current program scene UUID (Deprecated)
 *
 * @requestType GetCurrentProgramScene
 * @complexity 1
 * @rpcVersion -1
 * @initialVersion 5.0.0
 * @api requests
 * @category scenes
 */
RequestResult RequestHandler::GetCurrentProgramScene(const Request &)
{
	json responseData;
	OBSSourceAutoRelease currentProgramScene = obs_frontend_get_current_scene();
	responseData["sceneName"] = responseData["currentProgramSceneName"] = obs_source_get_name(currentProgramScene);
	responseData["sceneUuid"] = responseData["currentProgramSceneUuid"] = obs_source_get_uuid(currentProgramScene);

	return RequestResult::Success(responseData);
}

/**
 * Sets the current program scene.
 *
 * @requestField ?sceneName  | String | Scene name to set as the current program scene
 * @requestField ?sceneUuid  | String | Scene UUID to set as the current program scene
 *
 * @requestType SetCurrentProgramScene
 * @complexity 1
 * @rpcVersion -1
 * @initialVersion 5.0.0
 * @api requests
 * @category scenes
 */
RequestResult RequestHandler::SetCurrentProgramScene(const Request &request)
{
	RequestStatus::RequestStatus statusCode;
	std::string comment;
	OBSSourceAutoRelease scene = request.AcquireScene(statusCode, comment);
	if (!scene)
		return RequestResult::Error(statusCode, comment);

	OBSCanvasAutoRelease canvas = obs_source_get_canvas(scene);
	if (!canvas || !(obs_canvas_get_flags(canvas) & MAIN))
		return RequestResult::Error(
			RequestStatus::InvalidResourceState,
			"The specified scene is not from the main canvas and cannot be set as the program scene.");

	obs_frontend_set_current_scene(scene);

	return RequestResult::Success();
}

/**
 * Gets the current preview scene.
 *
 * Only available when studio mode is enabled.
 *
 * Note: This request is slated to have the `currentPreview`-prefixed fields removed from in an upcoming RPC version.
 *
 * @responseField sceneName               | String | Current preview scene name
 * @responseField sceneUuid               | String | Current preview scene UUID
 * @responseField currentPreviewSceneName | String | Current preview scene name
 * @responseField currentPreviewSceneUuid | String | Current preview scene UUID
 *
 * @requestType GetCurrentPreviewScene
 * @complexity 1
 * @rpcVersion -1
 * @initialVersion 5.0.0
 * @api requests
 * @category scenes
 */
RequestResult RequestHandler::GetCurrentPreviewScene(const Request &)
{
	if (!obs_frontend_preview_program_mode_active())
		return RequestResult::Error(RequestStatus::StudioModeNotActive);

	OBSSourceAutoRelease currentPreviewScene = obs_frontend_get_current_preview_scene();

	json responseData;
	responseData["sceneName"] = responseData["currentPreviewSceneName"] = obs_source_get_name(currentPreviewScene);
	responseData["sceneUuid"] = responseData["currentPreviewSceneUuid"] = obs_source_get_uuid(currentPreviewScene);

	return RequestResult::Success(responseData);
}

/**
 * Sets the current preview scene.
 *
 * Only available when studio mode is enabled.
 *
 * @requestField ?sceneName  | String | Scene name to set as the current preview scene
 * @requestField ?sceneUuid  | String | Scene UUID to set as the current preview scene
 *
 * @requestType SetCurrentPreviewScene
 * @complexity 1
 * @rpcVersion -1
 * @initialVersion 5.0.0
 * @api requests
 * @category scenes
 */
RequestResult RequestHandler::SetCurrentPreviewScene(const Request &request)
{
	if (!obs_frontend_preview_program_mode_active())
		return RequestResult::Error(RequestStatus::StudioModeNotActive);

	RequestStatus::RequestStatus statusCode;
	std::string comment;
	OBSSourceAutoRelease scene = request.AcquireScene(statusCode, comment);
	if (!scene)
		return RequestResult::Error(statusCode, comment);

	OBSCanvasAutoRelease canvas = obs_source_get_canvas(scene);
	if (!canvas || !(obs_canvas_get_flags(canvas) & MAIN))
		return RequestResult::Error(
			RequestStatus::InvalidResourceState,
			"The specified scene is not from the main canvas and cannot be set as the preview scene.");

	obs_frontend_set_current_preview_scene(scene);

	return RequestResult::Success();
}

/**
 * Creates a new scene in OBS.
 *
 * @requestField ?canvasUuid | String | UUID of the canvas to create the new scene in. Leave default to assume main canvas
 * @requestField sceneName   | String | Name for the new scene
 *
 * @responseField sceneUuid | String | UUID of the created scene
 *
 * @requestType CreateScene
 * @complexity 2
 * @rpcVersion -1
 * @initialVersion 5.0.0
 * @api requests
 * @category scenes
 */
RequestResult RequestHandler::CreateScene(const Request &request)
{
	RequestStatus::RequestStatus statusCode;
	std::string comment;
	if (!request.ValidateString("sceneName", statusCode, comment))
		return RequestResult::Error(statusCode, comment);

	std::string sceneName = request.RequestData["sceneName"];

	OBSCanvasAutoRelease canvas = request.AcquireCanvas("canvasUuid", statusCode, comment);
	if (!canvas)
		return RequestResult::Error(statusCode, comment);

	OBSSourceAutoRelease scene = obs_canvas_get_source_by_name(canvas, sceneName.c_str());
	if (scene)
		return RequestResult::Error(RequestStatus::ResourceAlreadyExists, "A source already exists by that scene name.");

	OBSSceneAutoRelease createdScene = obs_canvas_scene_create(canvas, sceneName.c_str());
	if (!createdScene)
		return RequestResult::Error(RequestStatus::ResourceCreationFailed, "Failed to create the scene.");

	json responseData;
	responseData["sceneUuid"] = obs_source_get_uuid(obs_scene_get_source(createdScene));

	return RequestResult::Success(responseData);
}

/**
 * Removes a scene from OBS.
 *
 * @requestField ?canvasUuid | String | UUID of the canvas the scene is in, if using the sceneName field
 * @requestField ?sceneName | String | Name of the scene to remove
 * @requestField ?sceneUuid | String | UUID of the scene to remove
 *
 * @requestType RemoveScene
 * @complexity 2
 * @rpcVersion -1
 * @initialVersion 5.0.0
 * @api requests
 * @category scenes
 */
RequestResult RequestHandler::RemoveScene(const Request &request)
{
	RequestStatus::RequestStatus statusCode;
	std::string comment;
	OBSSourceAutoRelease scene = request.AcquireScene(statusCode, comment);
	if (!scene)
		return RequestResult::Error(statusCode, comment);

	OBSCanvasAutoRelease canvas = obs_source_get_canvas(scene);
	if (!canvas)
		return RequestResult::Error(RequestStatus::ResourceActionFailed, "Error getting canvas for the specified scene.");

	if ((obs_canvas_get_flags(canvas) & MAIN) && Utils::Obs::NumberHelper::GetCanvasSceneCount(canvas) < 2)
		return RequestResult::Error(RequestStatus::NotEnoughResources,
					    "You cannot remove the last scene in the collection.");

	obs_source_remove(scene);

	return RequestResult::Success();
}

/**
 * Sets the name of a scene (rename).
 *
 * @requestField ?canvasUuid  | String | UUID of the canvas the scene is in, if using the sceneName field
 * @requestField ?sceneName   | String | Name of the scene to be renamed
 * @requestField ?sceneUuid   | String | UUID of the scene to be renamed
 * @requestField newSceneName | String | New name for the scene
 *
 * @requestType SetSceneName
 * @complexity 2
 * @rpcVersion -1
 * @initialVersion 5.0.0
 * @api requests
 * @category scenes
 */
RequestResult RequestHandler::SetSceneName(const Request &request)
{
	RequestStatus::RequestStatus statusCode;
	std::string comment;
	OBSSourceAutoRelease scene = request.AcquireScene(statusCode, comment);
	if (!(scene && request.ValidateString("newSceneName", statusCode, comment)))
		return RequestResult::Error(statusCode, comment);

	std::string newSceneName = request.RequestData["newSceneName"];

	OBSCanvasAutoRelease canvas = obs_source_get_canvas(scene);
	OBSSourceAutoRelease existingSource = obs_canvas_get_source_by_name(canvas, newSceneName.c_str());
	if (existingSource)
		return RequestResult::Error(RequestStatus::ResourceAlreadyExists,
					    "A source already exists by that new scene name.");

	obs_source_set_name(scene, newSceneName.c_str());

	return RequestResult::Success();
}

/**
 * Gets the scene transition overridden for a scene.
 *
 * Note: A transition UUID response field is not currently able to be implemented as of 2024-1-18.
 *
 * @requestField ?canvasUuid | String | UUID of the canvas the scene is in, if using the sceneName field
 * @requestField ?sceneName  | String | Name of the scene
 * @requestField ?sceneUuid  | String | UUID of the scene
 *
 * @responseField transitionName     | String | Name of the overridden scene transition, else `null`
 * @responseField transitionDuration | Number | Duration of the overridden scene transition, else `null`
 *
 * @requestType GetSceneSceneTransitionOverride
 * @complexity 2
 * @rpcVersion -1
 * @initialVersion 5.0.0
 * @api requests
 * @category scenes
 */
RequestResult RequestHandler::GetSceneSceneTransitionOverride(const Request &request)
{
	RequestStatus::RequestStatus statusCode;
	std::string comment;
	OBSSourceAutoRelease scene = request.AcquireScene(statusCode, comment);
	if (!scene)
		return RequestResult::Error(statusCode, comment);

	OBSDataAutoRelease privateSettings = obs_source_get_private_settings(scene);

	json responseData;
	const char *transitionName = obs_data_get_string(privateSettings, "transition");
	if (transitionName && strlen(transitionName))
		responseData["transitionName"] = transitionName;
	else
		responseData["transitionName"] = nullptr;

	if (obs_data_has_user_value(privateSettings, "transition_duration"))
		responseData["transitionDuration"] = obs_data_get_int(privateSettings, "transition_duration");
	else
		responseData["transitionDuration"] = nullptr;

	return RequestResult::Success(responseData);
}

/**
 * Sets the scene transition overridden for a scene.
 *
 * @requestField ?canvasUuid         | String | UUID of the canvas the scene is in, if using the sceneName field
 * @requestField ?sceneName          | String | Name of the scene
 * @requestField ?sceneUuid          | String | UUID of the scene
 * @requestField ?transitionName     | String | Name of the scene transition to use as override. Specify `null` to remove | Unchanged
 * @requestField ?transitionDuration | Number | Duration to use for any overridden transition. Specify `null` to remove | >= 50, <= 20000 | Unchanged
 *
 * @requestType SetSceneSceneTransitionOverride
 * @complexity 2
 * @rpcVersion -1
 * @initialVersion 5.0.0
 * @api requests
 * @category scenes
 */
RequestResult RequestHandler::SetSceneSceneTransitionOverride(const Request &request)
{
	RequestStatus::RequestStatus statusCode;
	std::string comment;
	OBSSourceAutoRelease scene = request.AcquireScene(statusCode, comment);
	if (!scene)
		return RequestResult::Error(statusCode, comment);

	OBSDataAutoRelease privateSettings = obs_source_get_private_settings(scene);

	bool hasName = request.RequestData.contains("transitionName");
	if (hasName && !request.RequestData["transitionName"].is_null()) {
		if (!request.ValidateOptionalString("transitionName", statusCode, comment))
			return RequestResult::Error(statusCode, comment);
		OBSSourceAutoRelease transition =
			Utils::Obs::SearchHelper::GetSceneTransitionByName(request.RequestData["transitionName"]);
		if (!transition)
			return RequestResult::Error(RequestStatus::ResourceNotFound, "No scene transition was found by that name.");
	}

	bool hasDuration = request.RequestData.contains("transitionDuration");
	if (hasDuration && !request.RequestData["transitionDuration"].is_null()) {
		if (!request.ValidateOptionalNumber("transitionDuration", statusCode, comment, 50, 20000))
			return RequestResult::Error(statusCode, comment);
	}

	if (!hasName && !hasDuration)
		return RequestResult::Error(RequestStatus::MissingRequestField,
					    "Your request data must include either `transitionName` or `transitionDuration`.");

	if (hasName) {
		if (request.RequestData["transitionName"].is_null()) {
			obs_data_erase(privateSettings, "transition");
		} else {
			std::string transitionName = request.RequestData["transitionName"];
			obs_data_set_string(privateSettings, "transition", transitionName.c_str());
		}
	}

	if (hasDuration) {
		if (request.RequestData["transitionDuration"].is_null()) {
			obs_data_erase(privateSettings, "transition_duration");
		} else {
			obs_data_set_int(privateSettings, "transition_duration", request.RequestData["transitionDuration"]);
		}
	}

	return RequestResult::Success();
}
