/*
 * uterm - Linux User-Space Terminal
 *
 * Copyright (c) 2011-2012 David Herrmann <dh.herrmann@googlemail.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files
 * (the "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

/*
 * Video Control
 * Core Implementation of the uterm_video and uterm_display objects.
 */

#include <errno.h>
#include <inttypes.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "eloop.h"
#include "log.h"
#include "shl_hook.h"
#include "uterm.h"
#include "uterm_video.h"

#define LOG_SUBSYSTEM "video"

const char *uterm_dpms_to_name(int dpms)
{
	switch (dpms) {
	case UTERM_DPMS_ON:
		return "ON";
	case UTERM_DPMS_STANDBY:
		return "STANDBY";
	case UTERM_DPMS_SUSPEND:
		return "SUSPEND";
	case UTERM_DPMS_OFF:
		return "OFF";
	default:
		return "UNKNOWN";
	}
}

bool uterm_video_available(unsigned int type)
{
	switch (type) {
	case UTERM_VIDEO_DRM:
#ifdef BUILD_ENABLE_VIDEO_DRM
		return video_drm_available();
#endif
		return false;
	case UTERM_VIDEO_DUMB:
#ifdef BUILD_ENABLE_VIDEO_DUMB
		return video_drm_available();
#endif
		return false;
	case UTERM_VIDEO_FBDEV:
#ifdef BUILD_ENABLE_VIDEO_FBDEV
		return true;
#endif
		return false;
	default:
		return false;
	}
}

int mode_new(struct uterm_mode **out, const struct mode_ops *ops)
{
	struct uterm_mode *mode;
	int ret;

	if (!out || !ops)
		return -EINVAL;

	mode = malloc(sizeof(*mode));
	if (!mode)
		return -ENOMEM;
	memset(mode, 0, sizeof(*mode));
	mode->ref = 1;
	mode->ops = ops;

	ret = VIDEO_CALL(mode->ops->init, 0, mode);
	if (ret)
		goto err_free;

	*out = mode;
	return 0;

err_free:
	free(mode);
	return ret;
}

void uterm_mode_ref(struct uterm_mode *mode)
{
	if (!mode || !mode->ref)
		return;

	++mode->ref;
}

void uterm_mode_unref(struct uterm_mode *mode)
{
	if (!mode || !mode->ref || --mode->ref)
		return;

	VIDEO_CALL(mode->ops->destroy, 0, mode);
	free(mode);
}

struct uterm_mode *uterm_mode_next(struct uterm_mode *mode)
{
	if (!mode)
		return NULL;

	return mode->next;
}

const char *uterm_mode_get_name(const struct uterm_mode *mode)
{
	if (!mode)
		return NULL;

	return VIDEO_CALL(mode->ops->get_name, NULL, mode);
}

unsigned int uterm_mode_get_width(const struct uterm_mode *mode)
{
	if (!mode)
		return 0;

	return VIDEO_CALL(mode->ops->get_width, 0, mode);
}

unsigned int uterm_mode_get_height(const struct uterm_mode *mode)
{
	if (!mode)
		return 0;

	return VIDEO_CALL(mode->ops->get_height, 0, mode);
}

int display_schedule_vblank_timer(struct uterm_display *disp)
{
	int ret;

	if (disp->vblank_scheduled)
		return 0;

	ret = ev_timer_update(disp->vblank_timer, &disp->vblank_spec);
	if (ret)
		return ret;

	disp->vblank_scheduled = true;
	return 0;
}

void display_set_vblank_timer(struct uterm_display *disp,
			      unsigned int msecs)
{
	if (msecs >= 1000)
		msecs = 999;
	else if (msecs == 0)
		msecs = 15;

	disp->vblank_spec.it_value.tv_nsec = msecs * 1000 * 1000;
}

static void display_vblank_timer_event(struct ev_timer *timer,
				       uint64_t expirations,
				       void *data)
{
	struct uterm_display *disp = data;

	disp->vblank_scheduled = false;
	DISPLAY_CB(disp, UTERM_PAGE_FLIP);
}

int display_new(struct uterm_display **out, const struct display_ops *ops,
		struct uterm_video *video)
{
	struct uterm_display *disp;
	int ret;

	if (!out || !ops || !video)
		return -EINVAL;

	disp = malloc(sizeof(*disp));
	if (!disp)
		return -ENOMEM;
	memset(disp, 0, sizeof(*disp));
	disp->ref = 1;
	disp->ops = ops;
	disp->video = video;

	ret = shl_hook_new(&disp->hook);
	if (ret)
		goto err_free;

	disp->vblank_spec.it_value.tv_nsec = 15 * 1000 * 1000;

	ret = ev_timer_new(&disp->vblank_timer, NULL,
			   display_vblank_timer_event, disp, NULL);
	if (ret)
		goto err_hook;

	ret = ev_eloop_add_timer(video->eloop, disp->vblank_timer);
	if (ret)
		goto err_timer;

	ret = VIDEO_CALL(disp->ops->init, 0, disp);
	if (ret)
		goto err_eloop_timer;

	log_info("new display %p", disp);
	*out = disp;
	return 0;

err_eloop_timer:
	ev_eloop_rm_timer(disp->vblank_timer);
err_timer:
	ev_timer_unref(disp->vblank_timer);
err_hook:
	shl_hook_free(disp->hook);
err_free:
	free(disp);
	return ret;
}

void uterm_display_ref(struct uterm_display *disp)
{
	if (!disp || !disp->ref)
		return;

	++disp->ref;
}

void uterm_display_unref(struct uterm_display *disp)
{
	struct uterm_mode *mode;

	if (!disp || !disp->ref || --disp->ref)
		return;

	log_info("free display %p", disp);

	VIDEO_CALL(disp->ops->destroy, 0, disp);

	while ((mode = disp->modes)) {
		disp->modes = mode->next;
		mode->next = NULL;
		uterm_mode_unref(mode);
	}
	ev_eloop_rm_timer(disp->vblank_timer);
	ev_timer_unref(disp->vblank_timer);
	shl_hook_free(disp->hook);
	free(disp);
}

struct uterm_display *uterm_display_next(struct uterm_display *disp)
{
	if (!disp)
		return NULL;

	return disp->next;
}

int uterm_display_register_cb(struct uterm_display *disp, uterm_display_cb cb,
			      void *data)
{
	if (!disp)
		return -EINVAL;

	return shl_hook_add_cast(disp->hook, cb, data);
}

void uterm_display_unregister_cb(struct uterm_display *disp,
				 uterm_display_cb cb, void *data)
{
	if (!disp)
		return;

	shl_hook_rm_cast(disp->hook, cb, data);
}

struct uterm_mode *uterm_display_get_modes(struct uterm_display *disp)
{
	if (!disp)
		return NULL;

	return disp->modes;
}

struct uterm_mode *uterm_display_get_current(struct uterm_display *disp)
{
	if (!disp)
		return NULL;

	return disp->current_mode;
}

struct uterm_mode *uterm_display_get_default(struct uterm_display *disp)
{
	if (!disp)
		return NULL;

	return disp->default_mode;
}

int uterm_display_get_state(struct uterm_display *disp)
{
	if (!disp)
		return UTERM_DISPLAY_GONE;

	if (disp->video) {
		if (disp->flags & DISPLAY_ONLINE) {
			if (disp->video->flags & VIDEO_AWAKE)
				return UTERM_DISPLAY_ACTIVE;
			else
				return UTERM_DISPLAY_ASLEEP;
		} else {
			return UTERM_DISPLAY_INACTIVE;
		}
	} else {
		return UTERM_DISPLAY_GONE;
	}
}

int uterm_display_activate(struct uterm_display *disp, struct uterm_mode *mode)
{
	if (!disp || !display_is_conn(disp) || display_is_online(disp))
		return -EINVAL;

	if (!mode)
		mode = disp->default_mode;

	return VIDEO_CALL(disp->ops->activate, 0, disp, mode);
}

void uterm_display_deactivate(struct uterm_display *disp)
{
	if (!disp || !display_is_online(disp))
		return;

	VIDEO_CALL(disp->ops->deactivate, 0, disp);
}

int uterm_display_set_dpms(struct uterm_display *disp, int state)
{
	if (!disp || !display_is_conn(disp))
		return -EINVAL;

	return VIDEO_CALL(disp->ops->set_dpms, 0, disp, state);
}

int uterm_display_get_dpms(const struct uterm_display *disp)
{
	if (!disp || !display_is_conn(disp))
		return UTERM_DPMS_OFF;

	return disp->dpms;
}

int uterm_display_use(struct uterm_display *disp)
{
	if (!disp || !display_is_online(disp))
		return -EINVAL;

	return VIDEO_CALL(disp->ops->use, -EOPNOTSUPP, disp);
}

int uterm_display_swap(struct uterm_display *disp)
{
	if (!disp || !display_is_online(disp))
		return -EINVAL;

	return VIDEO_CALL(disp->ops->swap, 0, disp);
}

bool uterm_display_is_swapping(struct uterm_display *disp)
{
	if (!disp)
		return false;

	return disp->vblank_scheduled || (disp->flags & DISPLAY_VSYNC);
}

int uterm_display_fill(struct uterm_display *disp,
		       uint8_t r, uint8_t g, uint8_t b,
		       unsigned int x, unsigned int y,
		       unsigned int width, unsigned int height)
{
	if (!disp)
		return -EINVAL;

	return VIDEO_CALL(disp->ops->fill, -EOPNOTSUPP, disp, r, g, b, x, y,
			  width, height);
}

int uterm_display_blit(struct uterm_display *disp,
		       const struct uterm_video_buffer *buf,
		       unsigned int x, unsigned int y)
{
	if (!disp)
		return -EINVAL;

	return VIDEO_CALL(disp->ops->blit, -EOPNOTSUPP, disp, buf, x, y);
}

int uterm_display_fake_blend(struct uterm_display *disp,
			     const struct uterm_video_buffer *buf,
			     unsigned int x, unsigned int y,
			     uint8_t fr, uint8_t fg, uint8_t fb,
			     uint8_t br, uint8_t bg, uint8_t bb)
{
	struct uterm_video_blend_req req;

	if (!disp)
		return -EINVAL;

	memset(&req, 0, sizeof(req));
	req.buf = buf;
	req.x = x;
	req.y = y;
	req.fr = fr;
	req.fg = fg;
	req.fb = fb;
	req.br = br;
	req.bg = bg;
	req.bb = bb;

	return VIDEO_CALL(disp->ops->fake_blendv, -EOPNOTSUPP, disp, &req, 1);
}

int uterm_display_fake_blendv(struct uterm_display *disp,
			      const struct uterm_video_blend_req *req,
			      size_t num)
{
	if (!disp)
		return -EINVAL;

	return VIDEO_CALL(disp->ops->fake_blendv, -EOPNOTSUPP, disp, req, num);
}

int uterm_video_new(struct uterm_video **out,
			struct ev_eloop *eloop,
			unsigned int type,
			const char *node)
{
	struct uterm_video *video;
	int ret;
	const struct video_ops *ops;

	if (!out || !eloop)
		return -EINVAL;

	switch (type) {
	case UTERM_VIDEO_DRM:
		if (!drm_available) {
			log_err("DRM backend is not available");
			return -EOPNOTSUPP;
		}
		ops = &drm_video_ops;
		break;
	case UTERM_VIDEO_DUMB:
		if (!dumb_available) {
			log_err("Dumb DRM backend is not available");
			return -EOPNOTSUPP;
		}
		ops = &dumb_video_ops;
		break;
	case UTERM_VIDEO_FBDEV:
		if (!fbdev_available) {
			log_err("FBDEV backend is not available");
			return -EOPNOTSUPP;
		}
		ops = &fbdev_video_ops;
		break;
	default:
		log_err("invalid video backend %d", type);
		return -EINVAL;
	}

	video = malloc(sizeof(*video));
	if (!video)
		return -ENOMEM;
	memset(video, 0, sizeof(*video));
	video->ref = 1;
	video->ops = ops;
	video->eloop = eloop;

	ret = shl_hook_new(&video->hook);
	if (ret)
		goto err_free;

	ret = VIDEO_CALL(video->ops->init, 0, video, node);
	if (ret)
		goto err_hook;

	ev_eloop_ref(video->eloop);
	log_info("new device %p", video);
	*out = video;
	return 0;

err_hook:
	shl_hook_free(video->hook);
err_free:
	free(video);
	return ret;
}

void uterm_video_ref(struct uterm_video *video)
{
	if (!video || !video->ref)
		return;

	++video->ref;
}

void uterm_video_unref(struct uterm_video *video)
{
	struct uterm_display *disp;

	if (!video || !video->ref || --video->ref)
		return;

	log_info("free device %p", video);

	VIDEO_CALL(video->ops->destroy, 0, video);

	while ((disp = video->displays)) {
		video->displays = disp->next;
		disp->next = NULL;
		uterm_display_unref(disp);
	}

	shl_hook_free(video->hook);
	ev_eloop_unref(video->eloop);
	free(video);
}

void uterm_video_segfault(struct uterm_video *video)
{
	if (!video)
		return;

	VIDEO_CALL(video->ops->segfault, 0, video);
}

int uterm_video_use(struct uterm_video *video)
{
	if (!video)
		return -EINVAL;

	return video_do_use(video);
}

struct uterm_display *uterm_video_get_displays(struct uterm_video *video)
{
	if (!video)
		return NULL;

	return video->displays;
}

int uterm_video_register_cb(struct uterm_video *video, uterm_video_cb cb,
				void *data)
{
	if (!video || !cb)
		return -EINVAL;

	return shl_hook_add_cast(video->hook, cb, data);
}

void uterm_video_unregister_cb(struct uterm_video *video, uterm_video_cb cb,
				void *data)
{
	if (!video || !cb)
		return;

	shl_hook_rm_cast(video->hook, cb, data);
}

void uterm_video_sleep(struct uterm_video *video)
{
	if (!video || !video_is_awake(video))
		return;

	VIDEO_CB(video, NULL, UTERM_SLEEP);
	VIDEO_CALL(video->ops->sleep, 0, video);
}

int uterm_video_wake_up(struct uterm_video *video)
{
	int ret;

	if (!video)
		return -EINVAL;
	if (video_is_awake(video))
		return 0;

	ret = VIDEO_CALL(video->ops->wake_up, 0, video);
	if (ret)
		return ret;

	VIDEO_CB(video, NULL, UTERM_WAKE_UP);
	return 0;
}

bool uterm_video_is_awake(struct uterm_video *video)
{
	return video && video_is_awake(video);
}

void uterm_video_poll(struct uterm_video *video)
{
	if (!video)
		return;

	VIDEO_CALL(video->ops->poll, 0, video);
}
