/*
 * \brief  Port of VirtualBox to Genode
 * \author Norman Feske
 * \author Alexander Boettcher
 * \author Christian Helmuth
 * \date   2013-08-20
 */

/*
 * Copyright (C) 2013-2021 Genode Labs GmbH
 *
 * This file is distributed under the terms of the GNU General Public License
 * version 2.
 */

/* Genode includes */
#include <base/attached_rom_dataspace.h>
#include <base/registry.h>
#include <libc/component.h>
#include <libc/args.h>

/* Virtualbox includes */
#include <nsXPCOM.h>
#include <nsCOMPtr.h>
#include <iprt/initterm.h>
#include <iprt/err.h>
#include <VBox/com/listeners.h>

/* Virtualbox includes of generic Main frontend */
#include "ConsoleImpl.h"
#include "MachineImpl.h"
#include "MouseImpl.h"
#include "SessionImpl.h"
#include "VirtualBoxImpl.h"

/* Genode port specific includes */
#include <attempt.h>
#include <init.h>
#include <fb.h>
#include <input_adapter.h>

using namespace Genode;


struct Event_handler : Interface
{
	virtual void handle_vbox_event(VBoxEventType_T, IEvent &) = 0;
};


struct Event_listener
{
	Event_handler *_handler_ptr = nullptr;

	virtual ~Event_listener() { }

	HRESULT init(Event_handler &handler)
	{
		_handler_ptr = &handler;
		return S_OK;
	}

	void uninit() { }

	STDMETHOD(HandleEvent)(VBoxEventType_T ev_type, IEvent *ev)
	{
		if (_handler_ptr)
			_handler_ptr->handle_vbox_event(ev_type, *ev);

		return S_OK;
	}
};

typedef ListenerImpl<Event_listener, Event_handler &> Event_listener_impl;

VBOX_LISTENER_DECLARE(Event_listener_impl)


struct Main : Event_handler
{
	Env &_env;

	Attached_rom_dataspace _config { _env, "config" };

	struct Vbox_file_path
	{
		typedef String<128> Path;

		Path const _path;

		com::Utf8Str const utf8 { _path.string() };

		Vbox_file_path(Xml_node config)
		:
			_path(config.attribute_value("vbox_file", Path()))
		{
			if (!_path.valid()) {
				error("missing 'vbox_file' attribute in config");
				throw Fatal();
			}
		}
	} _vbox_file_path { _config.xml() };

	/*
	 * Create VirtualBox object
	 *
	 * We cannot create the object via 'ComObjPtr<VirtualBox>::createObject'
	 * because 'FinalConstruction' uses a temporary 'ComObjPtr<VirtualBox>'
	 * (implicitly constructed as argument for the 'ClientWatcher' constructor.
	 * Upon the destruction of the temporary, the 'VirtualBox' refcnt becomes
	 * zero, which prompts 'VirtualBox::Release' to destruct the object.
	 *
	 * To sidestep this suicidal behavior, we manually perform the steps of
	 * 'createObject' but calling 'AddRef' before 'FinalConstruct'.
	 */
	struct Virtualbox_instance : ComObjPtr<VirtualBox>
	{
		VirtualBox _instance;

		Virtualbox_instance()
		{
			_instance.AddRef();

			attempt([&] () { return _instance.FinalConstruct(); },
			        "construction of VirtualBox object failed");

			ComObjPtr<VirtualBox>::operator = (&_instance);
		}
	} _virtualbox { };

	struct Session_instance : ComObjPtr<::Session>
	{
		Session_instance()
		{
			attempt([&] () { return createObject(); },
			        "construction of VirtualBox session object failed");
		}
	} _session { };

	struct Monitor_count { PRUint32 value; };

	struct Machine_instance : ComObjPtr<Machine>
	{
		Machine_instance(Virtualbox_instance  &virtualbox,
		                 Session_instance     &session,
		                 Vbox_file_path const &vbox_file_path)
		{
			attempt([&] () { return createObject(); },
			        "failed to create Machine object");

			attempt([&] () { return (*this)->initFromSettings(virtualbox,
			                                                  vbox_file_path.utf8,
			                                                  nullptr); },
			        "failed to init machine from settings");

			/*
			 * Add the machine to the VirtualBox::allMachines list
			 *
			 * Unfortunately, the 'i_registerMachine' function performs a
			 * 'i_saveSettings' should the 'VirtualBox' object not be in the
			 * 'InInit' state. However, the object is already in 'Ready' state.
			 * So, 'i_saveSettings' attempts to write a 'VirtualBox.xml' file
			 */
			{
				AutoWriteLock alock(virtualbox.m_p COMMA_LOCKVAL_SRC_POS);

				attempt([&] () { return (*this)->i_prepareRegister(); },
				        "could not enter registered state for machine");
			}

			attempt([&] () { return (*this)->LockMachine(session, LockType_VM); },
			        "failed to lock machine");
		}

		Monitor_count monitor_count()
		{
			ComPtr<IGraphicsAdapter> adapter;

			attempt([&] () { return (*this)->COMGETTER(GraphicsAdapter)(adapter.asOutParam()); },
			        "attempt to access virtual graphics adapter failed");

			Monitor_count result { 0 };

			attempt([&] () { return adapter->COMGETTER(MonitorCount)(&result.value); },
			        "unable to determine the number of virtual monitors");

			return result;
		}

	} _machine { _virtualbox, _session, _vbox_file_path };

	struct Console_interface : ComPtr<IConsole>
	{
		Console_interface(Session_instance &session)
		{
			attempt([&] () { return session->COMGETTER(Console)(this->asOutParam()); },
			        "unable to request console for session");
		}
	} _iconsole { _session };

	struct Display_interface : ComPtr<IDisplay>
	{
		Display_interface(Console_interface &iconsole)
		{
			attempt([&] () { return iconsole->COMGETTER(Display)(this->asOutParam()); },
			        "unable to request display from console interface");
		}
	} _idisplay { _iconsole };

	Signal_handler<Main> _capslock_handler { _env.ep(), *this, &Main::_handle_capslock };

	void _handle_capslock();

	struct Capslock
	{
		enum class Mode { RAW, ROM } const mode;

		bool _host  { false };
		bool _guest { false };

		Constructible<Attached_rom_dataspace> _rom;

		Capslock(Env &env, Xml_node config, Signal_context_capability sigh)
		:
			mode(config.attribute_value("capslock", String<4>()) == "rom"
			   ? Mode::ROM : Mode::RAW)
		{
			if (mode == Mode::ROM) {
				_rom.construct(env, "capslock");
				_rom->sigh(sigh);
			}
		}

		void update_guest(bool enabled) { _guest = enabled; }

		bool update_from_rom()
		{
			_rom->update();

			bool const rom = _rom->xml().attribute_value("enabled", _guest);

			bool trigger = false;

			/*
			 * If guest didn't respond with led change last time, we have to
			 * trigger CapsLock change - mainly assuming that guest don't use the
			 * led to externalize its internal state.
			 */
			if (rom != _host && _host != _guest)
				trigger = true;

			if (rom != _guest)
				trigger = true;

			/* remember last seen host capslock state */
			_host = rom;

			return trigger;
		}
	} _capslock { _env, _config.xml(), _capslock_handler };

	Registry<Registered<Gui::Connection>> _gui_connections { };

	Signal_handler<Main> _input_handler { _env.ep(), *this, &Main::_handle_input };

	void _handle_input();

	Signal_handler<Main> _fb_mode_handler { _env.ep(), *this, &Main::_handle_fb_mode };

	void _handle_fb_mode();

	Input_adapter _input_adapter { _iconsole };

	bool const _genode_gui_attached = ( _attach_genode_gui(), true );

	void _attach_genode_gui()
	{
		Monitor_count const num_monitors = _machine.monitor_count();

		for (unsigned i = 0; i < num_monitors.value; i++) {

			String<3> label { i };

			Gui::Connection &gui = *new Registered<Gui::Connection>(_gui_connections, _env, label.string());

			gui.input()->sigh(_input_handler);
			gui.mode_sigh(_fb_mode_handler);

			Genodefb *fb = new Genodefb(_env, gui, _idisplay);

			Bstr fb_id { };

			attempt([&] () { return _idisplay->AttachFramebuffer(i, fb, fb_id.asOutParam()); },
			        "unable to attach framebuffer to virtual monitor ", i);
		}
	}

	bool const _machine_powered_up = ( _power_up_machine(), true );

	void _power_up_machine()
	{
		ComPtr <IProgress> progress;

		attempt([&] () { return _iconsole->PowerUp(progress.asOutParam()); },
		        "powering up via console interface failed");

		/* wait until VM is up */
		MachineState_T state = MachineState_Null;
		do {
			if (state != MachineState_Null)
				RTThreadSleep(1000);

			attempt([&] () { return _machine->COMGETTER(State)(&state); },
			        "failed to obtain machine state");

		} while (state == MachineState_Starting);

		if (state != MachineState_Running) {
			error("machine could not enter running state");
			throw Fatal();
		}
	}

	void handle_vbox_event(VBoxEventType_T, IEvent &) override;

	bool const _vbox_event_handler_installed = ( _install_vbox_event_handler(), true );

	void _install_vbox_event_handler()
	{
		ComObjPtr<Event_listener_impl> listener;

		listener.createObject();
		listener->init(new Event_listener(), *this);

		ComPtr<IEventSource> ievent_source;
		_iconsole->COMGETTER(EventSource)(ievent_source.asOutParam());

		com::SafeArray<VBoxEventType_T> event_types;
		event_types.push_back(VBoxEventType_OnMouseCapabilityChanged);
		event_types.push_back(VBoxEventType_OnMousePointerShapeChanged);
		event_types.push_back(VBoxEventType_OnKeyboardLedsChanged);

		ievent_source->RegisterListener(listener, ComSafeArrayAsInParam(event_types), true);
	}

	Main(Genode::Env &env) : _env(env) { }
};


void Main::_handle_capslock()
{
	if (_capslock.update_from_rom()) {
		Libc::with_libc([&] {
			_input_adapter.handle_input_event(Input::Event { Input::Press   { Input::KEY_CAPSLOCK } });
			_input_adapter.handle_input_event(Input::Event { Input::Release { Input::KEY_CAPSLOCK } });
		});
	}
}


void Main::_handle_input()
{
	auto handle_one_event = [&] (Input::Event const &ev) {
		/* don't confuse guests and drop CapsLock events in ROM mode  */
		if (_capslock.mode == Capslock::Mode::ROM) {
			if (ev.key_press(Input::KEY_CAPSLOCK)
			 || ev.key_release(Input::KEY_CAPSLOCK))
				return;
		}

		_input_adapter.handle_input_event(ev);
	};

	Libc::with_libc([&] {
		_gui_connections.for_each([&] (Gui::Connection &gui) {
			gui.input()->for_each_event([&] (Input::Event const &ev) {
				handle_one_event(ev); }); }); });
}


void Main::_handle_fb_mode()
{
	Libc::with_libc([&] {
		_gui_connections.for_each([&] (Gui::Connection &gui) {
			IFramebuffer *pFramebuffer = NULL;
			HRESULT rc = _idisplay->QueryFramebuffer(0, &pFramebuffer);
			Assert(SUCCEEDED(rc) && pFramebuffer);

			Genodefb *fb = dynamic_cast<Genodefb *>(pFramebuffer);

			fb->update_mode(gui.mode());

			if ((fb->w() <= 1) && (fb->h() <= 1)) {
				/* interpret a size of 0x0 as indication to quit VirtualBox */
				if (_iconsole->PowerButton() != S_OK)
					Genode::error("ACPI shutdown failed");
				return;
			}

			_idisplay->SetVideoModeHint(0 /*=display*/,
			                            true /*=enabled*/, false /*=changeOrigin*/,
			                            0 /*=originX*/, 0 /*=originY*/,
			                            fb->w(), fb->h(),
			                            32, true);
		});
	});
}


void Main::handle_vbox_event(VBoxEventType_T ev_type, IEvent &ev)
{
	switch (ev_type) {
	case VBoxEventType_OnMouseCapabilityChanged:
		{
			ComPtr<IMouseCapabilityChangedEvent> cap_ev = &ev;
			BOOL absolute;
			cap_ev->COMGETTER(SupportsAbsolute)(&absolute);
			_input_adapter.mouse_absolute(!!absolute);
		} break;

	case VBoxEventType_OnMousePointerShapeChanged: break;

	case VBoxEventType_OnKeyboardLedsChanged:
		{
			ComPtr<IKeyboardLedsChangedEvent> led_ev = &ev;
			BOOL capslock;
			led_ev->COMGETTER(CapsLock)(&capslock);
			_capslock.update_guest(!!capslock);
		} break;

	default: /* ignore other events */ break;
	}
}


/* initial environment for the FreeBSD libc implementation */
extern char **environ;


void Libc::Component::construct(Libc::Env &env)
{
	Libc::with_libc([&] () {

		/* extract args and environment variables from config */
		int argc    = 0;
		char **argv = nullptr;
		char **envp = nullptr;

		populate_args_and_env(env, argc, argv, envp);

		environ = envp;

		Pthread::init(env);
		Network::init(env);

		/* sidestep 'rtThreadPosixSelectPokeSignal' */
		uint32_t const fFlags = RTR3INIT_FLAGS_UNOBTRUSIVE;

		{
			int const rc = RTR3InitExe(argc, &argv, fFlags);
			if (RT_FAILURE(rc))
				throw -1;
		}

		{
			nsCOMPtr<nsIServiceManager> serviceManager;
			HRESULT const rc = NS_InitXPCOM2(getter_AddRefs(serviceManager), nsnull, nsnull);
			if (NS_FAILED(rc))
			{
				Genode::error("failed to initialize XPCOM, rc=", rc);
				throw -2;
			}
		}

		Sup::init(env);
		Xhci::init(env);

		try {
			static Main main(env);
		}
		catch (...) {
			error("startup of virtual machine failed, giving up.");
		}
	});
}


NS_IMPL_THREADSAFE_ISUPPORTS1_CI(Genodefb, IFramebuffer)
NS_DECL_CLASSINFO(Genodefb)
