/*
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 "calls/calls_instance.h"

#include "calls/calls_group_common.h"
#include "mtproto/mtproto_dh_utils.h"
#include "core/application.h"
#include "main/main_session.h"
#include "main/main_account.h"
#include "apiwrap.h"
#include "lang/lang_keys.h"
#include "boxes/confirm_box.h"
#include "calls/calls_call.h"
#include "calls/calls_group_call.h"
#include "calls/calls_panel.h"
#include "calls/calls_group_panel.h"
#include "data/data_user.h"
#include "data/data_group_call.h"
#include "data/data_channel.h"
#include "data/data_chat.h"
#include "data/data_session.h"
#include "media/audio/media_audio_track.h"
#include "platform/platform_specific.h"
#include "ui/toast/toast.h"
#include "base/unixtime.h"
#include "mainwidget.h"
#include "mtproto/mtproto_config.h"
#include "boxes/rate_call_box.h"
#include "app.h"

#include <tgcalls/VideoCaptureInterface.h>
#include <tgcalls/StaticThreads.h>

namespace Calls {
namespace {

constexpr auto kServerConfigUpdateTimeoutMs = 24 * 3600 * crl::time(1000);

} // namespace

Instance::Instance() = default;

Instance::~Instance() = default;

void Instance::startOutgoingCall(not_null<UserData*> user, bool video) {
	if (activateCurrentCall()) {
		return;
	}
	if (user->callsStatus() == UserData::CallsStatus::Private) {
		// Request full user once more to refresh the setting in case it was changed.
		user->session().api().requestFullPeer(user);
		Ui::show(Box<InformBox>(
			tr::lng_call_error_not_available(tr::now, lt_user, user->name)));
		return;
	}
	requestPermissionsOrFail(crl::guard(this, [=] {
		createCall(user, Call::Type::Outgoing, video);
	}), video);
}

void Instance::startOrJoinGroupCall(
		not_null<PeerData*> peer,
		const QString &joinHash,
		bool confirmNeeded) {
	const auto context = confirmNeeded
		? Group::ChooseJoinAsProcess::Context::JoinWithConfirm
		: peer->groupCall()
		? Group::ChooseJoinAsProcess::Context::Join
		: Group::ChooseJoinAsProcess::Context::Create;
	_chooseJoinAs.start(peer, context, [=](object_ptr<Ui::BoxContent> box) {
		Ui::show(std::move(box), Ui::LayerOption::KeepOther);
	}, [=](QString text) {
		Ui::Toast::Show(text);
	}, [=](Group::JoinInfo info) {
		const auto call = info.peer->groupCall();
		info.joinHash = joinHash;
		createGroupCall(
			std::move(info),
			call ? call->input() : MTP_inputGroupCall(MTPlong(), MTPlong()));
	});
}

void Instance::callFinished(not_null<Call*> call) {
	crl::on_main(call, [=] {
		destroyCall(call);
	});
}

void Instance::callFailed(not_null<Call*> call) {
	crl::on_main(call, [=] {
		destroyCall(call);
	});
}

void Instance::callRedial(not_null<Call*> call) {
	if (_currentCall.get() == call) {
		refreshDhConfig();
	}
}

void Instance::groupCallFinished(not_null<GroupCall*> call) {
	crl::on_main(call, [=] {
		destroyGroupCall(call);
	});
}

void Instance::groupCallFailed(not_null<GroupCall*> call) {
	crl::on_main(call, [=] {
		destroyGroupCall(call);
	});
}

not_null<Media::Audio::Track*> Instance::ensureSoundLoaded(
		const QString &key) {
	const auto i = _tracks.find(key);
	if (i != end(_tracks)) {
		return i->second.get();
	}
	const auto result = _tracks.emplace(
		key,
		Media::Audio::Current().createTrack()).first->second.get();
	result->fillFromFile(Core::App().settings().getSoundPath(key));
	return result;
}

void Instance::playSoundOnce(const QString &key) {
	ensureSoundLoaded(key)->playOnce();
}

void Instance::callPlaySound(CallSound sound) {
	playSoundOnce([&] {
		switch (sound) {
		case CallSound::Busy: return "call_busy";
		case CallSound::Ended: return "call_end";
		case CallSound::Connecting: return "call_connect";
		}
		Unexpected("CallSound in Instance::callPlaySound.");
		return "";
	}());
}

void Instance::groupCallPlaySound(GroupCallSound sound) {
	playSoundOnce([&] {
		switch (sound) {
		case GroupCallSound::Started: return "group_call_start";
		case GroupCallSound::Ended: return "group_call_end";
		case GroupCallSound::AllowedToSpeak: return "group_call_allowed";
		case GroupCallSound::Connecting: return "group_call_connect";
		}
		Unexpected("GroupCallSound in Instance::groupCallPlaySound.");
		return "";
	}());
}

void Instance::destroyCall(not_null<Call*> call) {
	if (_currentCall.get() == call) {
		_currentCallPanel->closeBeforeDestroy();
		_currentCallPanel = nullptr;

		auto taken = base::take(_currentCall);
		_currentCallChanges.fire(nullptr);
		taken.reset();

		if (App::quitting()) {
			LOG(("Calls::Instance doesn't prevent quit any more."));
		}
		Core::App().quitPreventFinished();
	}
}

void Instance::createCall(not_null<UserData*> user, Call::Type type, bool video) {
	auto call = std::make_unique<Call>(getCallDelegate(), user, type, video);
	const auto raw = call.get();

	user->session().account().sessionChanges(
	) | rpl::start_with_next([=] {
		destroyCall(raw);
	}, raw->lifetime());

	if (_currentCall) {
		_currentCallPanel->replaceCall(raw);
		std::swap(_currentCall, call);
		call->hangup();
	} else {
		_currentCallPanel = std::make_unique<Panel>(raw);
		_currentCall = std::move(call);
	}
	_currentCallChanges.fire_copy(raw);
	refreshServerConfig(&user->session());
	refreshDhConfig();
}

void Instance::destroyGroupCall(not_null<GroupCall*> call) {
	if (_currentGroupCall.get() == call) {
		_currentGroupCallPanel->closeBeforeDestroy();
		_currentGroupCallPanel = nullptr;

		auto taken = base::take(_currentGroupCall);
		_currentGroupCallChanges.fire(nullptr);
		taken.reset();

		if (App::quitting()) {
			LOG(("Calls::Instance doesn't prevent quit any more."));
		}
		Core::App().quitPreventFinished();
	}
}

void Instance::createGroupCall(
		Group::JoinInfo info,
		const MTPInputGroupCall &inputCall) {
	destroyCurrentCall();

	auto call = std::make_unique<GroupCall>(
		getGroupCallDelegate(),
		std::move(info),
		inputCall);
	const auto raw = call.get();

	info.peer->session().account().sessionChanges(
	) | rpl::start_with_next([=] {
		destroyGroupCall(raw);
	}, raw->lifetime());

	_currentGroupCallPanel = std::make_unique<Group::Panel>(raw);
	_currentGroupCall = std::move(call);
	_currentGroupCallChanges.fire_copy(raw);
}

void Instance::refreshDhConfig() {
	Expects(_currentCall != nullptr);

	const auto weak = base::make_weak(_currentCall);
	_currentCall->user()->session().api().request(MTPmessages_GetDhConfig(
		MTP_int(_dhConfig.version),
		MTP_int(MTP::ModExpFirst::kRandomPowerSize)
	)).done([=](const MTPmessages_DhConfig &result) {
		const auto call = weak.get();
		const auto random = updateDhConfig(result);
		if (!call) {
			return;
		}
		if (!random.empty()) {
			Assert(random.size() == MTP::ModExpFirst::kRandomPowerSize);
			call->start(random);
		} else {
			callFailed(call);
		}
	}).fail([=](const MTP::Error &error) {
		const auto call = weak.get();
		if (!call) {
			return;
		}
		callFailed(call);
	}).send();
}

bytes::const_span Instance::updateDhConfig(
		const MTPmessages_DhConfig &data) {
	const auto validRandom = [](const QByteArray & random) {
		if (random.size() != MTP::ModExpFirst::kRandomPowerSize) {
			return false;
		}
		return true;
	};
	return data.match([&](const MTPDmessages_dhConfig &data)
	-> bytes::const_span {
		auto primeBytes = bytes::make_vector(data.vp().v);
		if (!MTP::IsPrimeAndGood(primeBytes, data.vg().v)) {
			LOG(("API Error: bad p/g received in dhConfig."));
			return {};
		} else if (!validRandom(data.vrandom().v)) {
			return {};
		}
		_dhConfig.g = data.vg().v;
		_dhConfig.p = std::move(primeBytes);
		_dhConfig.version = data.vversion().v;
		return bytes::make_span(data.vrandom().v);
	}, [&](const MTPDmessages_dhConfigNotModified &data)
	-> bytes::const_span {
		if (!_dhConfig.g || _dhConfig.p.empty()) {
			LOG(("API Error: dhConfigNotModified on zero version."));
			return {};
		} else if (!validRandom(data.vrandom().v)) {
			return {};
		}
		return bytes::make_span(data.vrandom().v);
	});
}

void Instance::refreshServerConfig(not_null<Main::Session*> session) {
	if (_serverConfigRequestSession) {
		return;
	}
	if (_lastServerConfigUpdateTime
		&& ((crl::now() - _lastServerConfigUpdateTime)
			< kServerConfigUpdateTimeoutMs)) {
		return;
	}
	_serverConfigRequestSession = session;
	session->api().request(MTPphone_GetCallConfig(
	)).done([=](const MTPDataJSON &result) {
		_serverConfigRequestSession = nullptr;
		_lastServerConfigUpdateTime = crl::now();

		const auto &json = result.c_dataJSON().vdata().v;
		UpdateConfig(std::string(json.data(), json.size()));
	}).fail([=](const MTP::Error &error) {
		_serverConfigRequestSession = nullptr;
	}).send();
}

void Instance::handleUpdate(
		not_null<Main::Session*> session,
		const MTPUpdate &update) {
	update.match([&](const MTPDupdatePhoneCall &data) {
		handleCallUpdate(session, data.vphone_call());
	}, [&](const MTPDupdatePhoneCallSignalingData &data) {
		handleSignalingData(session, data);
	}, [&](const MTPDupdateGroupCall &data) {
		handleGroupCallUpdate(session, update);
	}, [&](const MTPDupdateGroupCallParticipants &data) {
		handleGroupCallUpdate(session, update);
	}, [](const auto &) {
		Unexpected("Update type in Calls::Instance::handleUpdate.");
	});
}

void Instance::showInfoPanel(not_null<Call*> call) {
	if (_currentCall.get() == call) {
		_currentCallPanel->showAndActivate();
	}
}

void Instance::showInfoPanel(not_null<GroupCall*> call) {
	if (_currentGroupCall.get() == call) {
		_currentGroupCallPanel->showAndActivate();
	}
}

void Instance::setCurrentAudioDevice(bool input, const QString &deviceId) {
	if (input) {
		Core::App().settings().setCallInputDeviceId(deviceId);
	} else {
		Core::App().settings().setCallOutputDeviceId(deviceId);
	}
	Core::App().saveSettingsDelayed();
	if (const auto call = currentCall()) {
		call->setCurrentAudioDevice(input, deviceId);
	} else if (const auto group = currentGroupCall()) {
		group->setCurrentAudioDevice(input, deviceId);
	}
}

bool Instance::isQuitPrevent() {
	if (!_currentCall || _currentCall->isIncomingWaiting()) {
		return false;
	}
	_currentCall->hangup();
	if (!_currentCall) {
		return false;
	}
	LOG(("Calls::Instance prevents quit, hanging up a call..."));
	return true;
}

void Instance::handleCallUpdate(
		not_null<Main::Session*> session,
		const MTPPhoneCall &call) {
	if (call.type() == mtpc_phoneCallRequested) {
		auto &phoneCall = call.c_phoneCallRequested();
		auto user = session->data().userLoaded(phoneCall.vadmin_id().v);
		if (!user) {
			LOG(("API Error: User not loaded for phoneCallRequested."));
		} else if (user->isSelf()) {
			LOG(("API Error: Self found in phoneCallRequested."));
		}
		const auto &config = session->serverConfig();
		if (inCall() || inGroupCall() || !user || user->isSelf()) {
			const auto flags = phoneCall.is_video()
				? MTPphone_DiscardCall::Flag::f_video
				: MTPphone_DiscardCall::Flag(0);
			session->api().request(MTPphone_DiscardCall(
				MTP_flags(flags),
				MTP_inputPhoneCall(phoneCall.vid(), phoneCall.vaccess_hash()),
				MTP_int(0),
				MTP_phoneCallDiscardReasonBusy(),
				MTP_long(0)
			)).send();
		} else if (phoneCall.vdate().v + (config.callRingTimeoutMs / 1000)
			< base::unixtime::now()) {
			LOG(("Ignoring too old call."));
		} else if (Core::App().settings().disableCalls()) {
			LOG(("Ignoring call because of 'accept calls' settings."));
		} else {
			createCall(user, Call::Type::Incoming, phoneCall.is_video());
			_currentCall->handleUpdate(call);
		}
	} else if (!_currentCall
		|| (&_currentCall->user()->session() != session)
		|| !_currentCall->handleUpdate(call)) {
		DEBUG_LOG(("API Warning: unexpected phone call update %1").arg(call.type()));
	}
}

void Instance::handleGroupCallUpdate(
		not_null<Main::Session*> session,
		const MTPUpdate &update) {
	const auto callId = update.match([](const MTPDupdateGroupCall &data) {
		return data.vcall().match([](const auto &data) {
			return data.vid().v;
		});
	}, [](const MTPDupdateGroupCallParticipants &data) {
		return data.vcall().match([&](const MTPDinputGroupCall &data) {
			return data.vid().v;
		});
	}, [](const auto &) -> uint64 {
		Unexpected("Type in Instance::handleGroupCallUpdate.");
	});
	if (const auto existing = session->data().groupCall(callId)) {
		existing->enqueueUpdate(update);
	} else {
		applyGroupCallUpdateChecked(session, update);
	}

	if (_currentGroupCall
		&& (&_currentGroupCall->peer()->session() == session)) {
		update.match([&](const MTPDupdateGroupCall &data) {
			_currentGroupCall->handlePossibleCreateOrJoinResponse(data);
		}, [](const auto &) {
		});
	}
}

void Instance::applyGroupCallUpdateChecked(
		not_null<Main::Session*> session,
		const MTPUpdate &update) {
	if (_currentGroupCall
		&& (&_currentGroupCall->peer()->session() == session)) {
		_currentGroupCall->handleUpdate(update);
	}
}

void Instance::handleSignalingData(
		not_null<Main::Session*> session,
		const MTPDupdatePhoneCallSignalingData &data) {
	if (!_currentCall
		|| (&_currentCall->user()->session() != session)
		|| !_currentCall->handleSignalingData(data)) {
		DEBUG_LOG(("API Warning: unexpected call signaling data %1"
			).arg(data.vphone_call_id().v));
	}
}

bool Instance::inCall() const {
	if (!_currentCall) {
		return false;
	}
	const auto state = _currentCall->state();
	return (state != Call::State::Busy);
}

bool Instance::inGroupCall() const {
	if (!_currentGroupCall) {
		return false;
	}
	const auto state = _currentGroupCall->state();
	return (state != GroupCall::State::HangingUp)
		&& (state != GroupCall::State::Ended)
		&& (state != GroupCall::State::FailedHangingUp)
		&& (state != GroupCall::State::Failed);
}

void Instance::destroyCurrentCall() {
	if (const auto current = currentCall()) {
		current->hangup();
		if (const auto still = currentCall()) {
			destroyCall(still);
		}
	}
	if (const auto current = currentGroupCall()) {
		current->hangup();
		if (const auto still = currentGroupCall()) {
			destroyGroupCall(still);
		}
	}
}

bool Instance::hasActivePanel(not_null<Main::Session*> session) const {
	if (inCall()) {
		return (&_currentCall->user()->session() == session)
			&& _currentCallPanel->isActive();
	} else if (inGroupCall()) {
		return (&_currentGroupCall->peer()->session() == session)
			&& _currentGroupCallPanel->isActive();
	}
	return false;
}

bool Instance::activateCurrentCall(const QString &joinHash) {
	if (inCall()) {
		_currentCallPanel->showAndActivate();
		return true;
	} else if (inGroupCall()) {
		if (!joinHash.isEmpty()) {
			_currentGroupCall->rejoinWithHash(joinHash);
		}
		_currentGroupCallPanel->showAndActivate();
		return true;
	}
	return false;
}

bool Instance::minimizeCurrentActiveCall() {
	if (inCall() && _currentCallPanel->isActive()) {
		_currentCallPanel->minimize();
		return true;
	} else if (inGroupCall() && _currentGroupCallPanel->isActive()) {
		_currentGroupCallPanel->minimize();
		return true;
	}
	return false;
}

bool Instance::closeCurrentActiveCall() {
	if (inGroupCall() && _currentGroupCallPanel->isActive()) {
		_currentGroupCallPanel->close();
		return true;
	}
	return false;
}

Call *Instance::currentCall() const {
	return _currentCall.get();
}

rpl::producer<Call*> Instance::currentCallValue() const {
	return _currentCallChanges.events_starting_with(currentCall());
}

GroupCall *Instance::currentGroupCall() const {
	return _currentGroupCall.get();
}

rpl::producer<GroupCall*> Instance::currentGroupCallValue() const {
	return _currentGroupCallChanges.events_starting_with(currentGroupCall());
}

void Instance::requestPermissionsOrFail(Fn<void()> onSuccess, bool video) {
	using Type = Platform::PermissionType;
	requestPermissionOrFail(Type::Microphone, [=] {
		auto callback = [=] { crl::on_main(onSuccess); };
		if (video) {
			requestPermissionOrFail(Type::Camera, std::move(callback));
		} else {
			callback();
		}
	});
}

void Instance::requestPermissionOrFail(Platform::PermissionType type, Fn<void()> onSuccess) {
	using Status = Platform::PermissionStatus;
	const auto status = Platform::GetPermissionStatus(type);
	if (status == Status::Granted) {
		onSuccess();
	} else if (status == Status::CanRequest) {
		Platform::RequestPermission(type, crl::guard(this, [=](Status status) {
			if (status == Status::Granted) {
				crl::on_main(onSuccess);
			} else {
				if (_currentCall) {
					_currentCall->hangup();
				}
			}
		}));
	} else {
		if (inCall()) {
			_currentCall->hangup();
		}
		if (inGroupCall()) {
			_currentGroupCall->hangup();
		}
		Ui::show(Box<ConfirmBox>(tr::lng_no_mic_permission(tr::now), tr::lng_menu_settings(tr::now), crl::guard(this, [=] {
			Platform::OpenSystemSettingsForPermission(type);
			Ui::hideLayer();
		})));
	}
}

std::shared_ptr<tgcalls::VideoCaptureInterface> Instance::getVideoCapture() {
	if (auto result = _videoCapture.lock()) {
		return result;
	}
	auto result = std::shared_ptr<tgcalls::VideoCaptureInterface>(
		tgcalls::VideoCaptureInterface::Create(
			tgcalls::StaticThreads::getThreads(),
			Core::App().settings().callVideoInputDeviceId().toStdString()));
	_videoCapture = result;
	return result;
}

} // namespace Calls
