/*
 * Copyright (c) 2022 Jiri Svoboda
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * - Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 * - Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 * - The name of the author may not be used to endorse or promote products
 *   derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

/** @addtogroup libwndmgt
 * @{
 */
/**
 * @file
 * @brief Window management protocol client
 */

#include <async.h>
#include <wndmgt.h>
#include <errno.h>
#include <fibril_synch.h>
#include <ipc/wndmgt.h>
#include <ipc/services.h>
#include <loc.h>
#include <mem.h>
#include <stdlib.h>
#include "../private/wndmgt.h"

static errno_t wndmgt_callback_create(wndmgt_t *);
static void wndmgt_cb_conn(ipc_call_t *, void *);

/** Open window management service.
 *
 * @param wmname Window management service name or @c NULL to use default
 * @param cb Window management callbacks
 * @param cb_arg Callback argument
 * @param rwndmgt Place to store pointer to window management session
 * @return EOK on success or an error code
 */
errno_t wndmgt_open(const char *wmname, wndmgt_cb_t *cb, void *cb_arg,
    wndmgt_t **rwndmgt)
{
	service_id_t wndmgt_svc;
	wndmgt_t *wndmgt;
	errno_t rc;

	wndmgt = calloc(1, sizeof(wndmgt_t));
	if (wndmgt == NULL)
		return ENOMEM;

	wndmgt->cb = cb;
	wndmgt->cb_arg = cb_arg;

	fibril_mutex_initialize(&wndmgt->lock);
	fibril_condvar_initialize(&wndmgt->cv);

	if (wmname == NULL)
		wmname = SERVICE_NAME_WNDMGT;

	rc = loc_service_get_id(wmname, &wndmgt_svc, 0);
	if (rc != EOK) {
		free(wndmgt);
		return ENOENT;
	}

	wndmgt->sess = loc_service_connect(wndmgt_svc, INTERFACE_WNDMGT,
	    0);
	if (wndmgt->sess == NULL) {
		free(wndmgt);
		return ENOENT;
	}

	rc = wndmgt_callback_create(wndmgt);
	if (rc != EOK) {
		async_hangup(wndmgt->sess);
		free(wndmgt);
		return EIO;
	}

	*rwndmgt = wndmgt;
	return EOK;
}

/** Create callback connection from window management service.
 *
 * @param wndmgt Window management
 * @return EOK on success or an error code
 */
static errno_t wndmgt_callback_create(wndmgt_t *wndmgt)
{
	async_exch_t *exch = async_exchange_begin(wndmgt->sess);

	aid_t req = async_send_0(exch, WNDMGT_CALLBACK_CREATE, NULL);

	port_id_t port;
	errno_t rc = async_create_callback_port(exch, INTERFACE_WNDMGT_CB, 0, 0,
	    wndmgt_cb_conn, wndmgt, &port);

	async_exchange_end(exch);

	if (rc != EOK)
		return rc;

	errno_t retval;
	async_wait_for(req, &retval);

	return retval;
}

/** Close window management service.
 *
 * @param wndmgt Window management
 */
void wndmgt_close(wndmgt_t *wndmgt)
{
	fibril_mutex_lock(&wndmgt->lock);
	async_hangup(wndmgt->sess);
	wndmgt->sess = NULL;

	/* Wait for callback handler to terminate */

	while (!wndmgt->cb_done)
		fibril_condvar_wait(&wndmgt->cv, &wndmgt->lock);
	fibril_mutex_unlock(&wndmgt->lock);

	free(wndmgt);
}

/** Get window list.
 *
 * @param wndmgt Window management
 * @param rlist Place to store pointer to new window list structure
 * @return EOK on success or an error code
 */
errno_t wndmgt_get_window_list(wndmgt_t *wndmgt, wndmgt_window_list_t **rlist)
{
	async_exch_t *exch;
	aid_t req;
	ipc_call_t answer;
	wndmgt_window_list_t *list;
	sysarg_t nwindows;
	sysarg_t *windows;
	errno_t rc;

	exch = async_exchange_begin(wndmgt->sess);
	req = async_send_0(exch, WNDMGT_GET_WINDOW_LIST, &answer);

	/* Receive window list length */
	rc = async_data_read_start(exch, &nwindows, sizeof (nwindows));
	if (rc != EOK) {
		async_exchange_end(exch);
		async_wait_for(req, &rc);
		return rc;
	}

	windows = calloc(nwindows, sizeof(sysarg_t));
	if (windows == NULL) {
		async_exchange_end(exch);
		async_forget(req);
		return ENOMEM;
	}

	/* Receive window list */
	rc = async_data_read_start(exch, windows, nwindows * sizeof (sysarg_t));
	async_exchange_end(exch);

	if (rc != EOK) {
		async_forget(req);
		return rc;
	}

	async_wait_for(req, &rc);
	if (rc != EOK)
		return rc;

	list = calloc(1, sizeof(wndmgt_window_list_t));
	if (list == NULL)
		return ENOMEM;

	list->nwindows = nwindows;
	list->windows = windows;
	*rlist = list;
	return EOK;
}

/** Free window list.
 *
 * @param list Window management list
 */
void wndmgt_free_window_list(wndmgt_window_list_t *list)
{
	free(list->windows);
	free(list);
}

/** Get window information.
 *
 * @param wndmgt Window management
 * @param wnd_id Window ID
 * @param rinfo Place to store pointer to new window information structure
 * @return EOK on success or an error code
 */
errno_t wndmgt_get_window_info(wndmgt_t *wndmgt, sysarg_t wnd_id,
    wndmgt_window_info_t **rinfo)
{
	async_exch_t *exch;
	aid_t req;
	ipc_call_t answer;
	wndmgt_window_info_t *info;
	sysarg_t capsize;
	char *caption;
	errno_t rc;

	exch = async_exchange_begin(wndmgt->sess);
	req = async_send_1(exch, WNDMGT_GET_WINDOW_INFO, wnd_id, &answer);

	/* Receive caption size */
	rc = async_data_read_start(exch, &capsize, sizeof (capsize));
	if (rc != EOK) {
		async_exchange_end(exch);
		async_wait_for(req, &rc);
		return rc;
	}

	caption = calloc(capsize + 1, sizeof(char));
	if (caption == NULL) {
		async_exchange_end(exch);
		async_forget(req);
		return ENOMEM;
	}

	/* Receive caption */
	rc = async_data_read_start(exch, caption, capsize);
	async_exchange_end(exch);

	if (rc != EOK) {
		async_forget(req);
		return rc;
	}

	async_wait_for(req, &rc);
	if (rc != EOK)
		return rc;

	/* Null-terminate the string */
	caption[capsize] = '\0';

	info = calloc(1, sizeof(wndmgt_window_info_t));
	if (info == NULL)
		return ENOMEM;

	info->caption = caption;
	*rinfo = info;
	return EOK;
}

/** Free window information.
 *
 * @param info Window management information
 */
void wndmgt_free_window_info(wndmgt_window_info_t *info)
{
	free(info->caption);
	free(info);
}

/** Activate window.
 *
 * @param wndmgt Window management session
 * @param seat_id Seat ID
 * @param wnd_id Window ID
 * @return EOK on success or an error code
 */
errno_t wndmgt_activate_window(wndmgt_t *wndmgt, sysarg_t seat_id,
    sysarg_t wnd_id)
{
	async_exch_t *exch;
	errno_t rc;

	exch = async_exchange_begin(wndmgt->sess);
	rc = async_req_2_0(exch, WNDMGT_ACTIVATE_WINDOW, seat_id,
	    wnd_id);

	async_exchange_end(exch);
	return rc;
}

/** Close window.
 *
 * @param wndmgt Window management
 * @param wnd_id Window ID
 * @return EOK on success or an error code
 */
errno_t wndmgt_close_window(wndmgt_t *wndmgt, sysarg_t wnd_id)
{
	async_exch_t *exch;
	errno_t rc;

	exch = async_exchange_begin(wndmgt->sess);
	rc = async_req_1_0(exch, WNDMGT_CLOSE_WINDOW, wnd_id);

	async_exchange_end(exch);
	return rc;
}

/** Get window management event.
 *
 * @param wndmgt Window management
 * @param event Place to store event
 * @return EOK on success or an error code
 */
static errno_t wndmgt_get_event(wndmgt_t *wndmgt, wndmgt_ev_t *event)
{
	async_exch_t *exch;
	ipc_call_t answer;
	aid_t req;
	errno_t rc;

	exch = async_exchange_begin(wndmgt->sess);
	req = async_send_0(exch, WNDMGT_GET_EVENT, &answer);
	rc = async_data_read_start(exch, event, sizeof(*event));
	async_exchange_end(exch);
	if (rc != EOK) {
		async_forget(req);
		return rc;
	}

	async_wait_for(req, &rc);
	if (rc != EOK)
		return rc;

	return EOK;
}

/** Window management events are pending.
 *
 * @param wndmgt Window management
 * @param icall Call data
 */
static void wndmgt_ev_pending(wndmgt_t *wndmgt, ipc_call_t *icall)
{
	errno_t rc;
	wndmgt_ev_t event;

	while (true) {
		fibril_mutex_lock(&wndmgt->lock);

		if (wndmgt->sess != NULL)
			rc = wndmgt_get_event(wndmgt, &event);
		else
			rc = ENOENT;

		fibril_mutex_unlock(&wndmgt->lock);

		if (rc != EOK)
			break;

		switch (event.etype) {
		case wmev_window_added:
			if (wndmgt->cb != NULL &&
			    wndmgt->cb->window_added != NULL) {
				wndmgt->cb->window_added(wndmgt->cb_arg,
				    event.wnd_id);
			}
			break;
		case wmev_window_removed:
			if (wndmgt->cb != NULL &&
			    wndmgt->cb->window_removed != NULL) {
				wndmgt->cb->window_removed(wndmgt->cb_arg,
				    event.wnd_id);
			}
			break;
		case wmev_window_changed:
			if (wndmgt->cb != NULL &&
			    wndmgt->cb->window_changed != NULL) {
				wndmgt->cb->window_changed(wndmgt->cb_arg,
				    event.wnd_id);
			}
			break;
		}
	}

	async_answer_0(icall, EOK);
}

/** Callback connection handler.
 *
 * @param icall Connect call data
 * @param arg   Argument, wndmgt_t *
 */
static void wndmgt_cb_conn(ipc_call_t *icall, void *arg)
{
	wndmgt_t *wndmgt = (wndmgt_t *) arg;

	while (true) {
		ipc_call_t call;
		async_get_call(&call);

		if (!ipc_get_imethod(&call)) {
			/* Hangup */
			async_answer_0(&call, EOK);
			goto out;
		}

		switch (ipc_get_imethod(&call)) {
		case WNDMGT_EV_PENDING:
			wndmgt_ev_pending(wndmgt, &call);
			break;
		default:
			async_answer_0(&call, ENOTSUP);
			break;
		}
	}

out:
	fibril_mutex_lock(&wndmgt->lock);
	wndmgt->cb_done = true;
	fibril_mutex_unlock(&wndmgt->lock);
	fibril_condvar_broadcast(&wndmgt->cv);
}

/** @}
 */
