/*-
 * Copyright 2003 Eric Anholt
 * All Rights Reserved.
 *
 * 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 (including the next
 * paragraph) 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
 * ERIC ANHOLT 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.
 *
 * Authors:
 *    Eric Anholt <anholt@FreeBSD.org>
 *
 */

/** @file drm_irq.c
 * Support code for handling setup/teardown of interrupt handlers and
 * handing interrupt handlers off to the drivers.
 */

#include <sys/workq.h>

#include "drmP.h"
#include "drm.h"

void		drm_update_vblank_count(struct drm_device *, int);
void		vblank_disable(void *);

#ifdef DRM_VBLANK_DEBUG
#define DPRINTF(x...)	do { printf(x); } while(/* CONSTCOND */ 0)
#else
#define DPRINTF(x...)	do { } while(/* CONSTCOND */ 0)
#endif

int
drm_irq_by_busid(struct drm_device *dev, void *data, struct drm_file *file_priv)
{
	struct drm_irq_busid	*irq = data;

	/*
	 * This is only ever called by root as part of a stupid interface.
	 * just hand over the irq without checking the busid. If all clients
	 * can be forced to use interface 1.2 then this can die.
	 */
	irq->irq = dev->irq;

	DRM_DEBUG("%d:%d:%d => IRQ %d\n", irq->busnum, irq->devnum,
	    irq->funcnum, irq->irq);

	return 0;
}

int
drm_irq_install(struct drm_device *dev)
{
	int	ret;

	if (dev->irq == 0 || dev->dev_private == NULL)
		return (EINVAL);

	DRM_DEBUG("irq=%d\n", dev->irq);

	DRM_LOCK();
	if (dev->irq_enabled) {
		DRM_UNLOCK();
		return (EBUSY);
	}
	dev->irq_enabled = 1;
	DRM_UNLOCK();

	if ((ret = dev->driver->irq_install(dev)) != 0)
		goto err;

	return (0);
err:
	DRM_LOCK();
	dev->irq_enabled = 0;
	DRM_UNLOCK();
	return (ret);
}

int
drm_irq_uninstall(struct drm_device *dev)
{
	int i;

	DRM_LOCK();
	if (!dev->irq_enabled) {
		DRM_UNLOCK();
		return (EINVAL);
	}

	dev->irq_enabled = 0;
	DRM_UNLOCK();

	/*
	 * Ick. we're about to turn of vblanks, so make sure anyone waiting
	 * on them gets woken up. Also make sure we update state correctly
	 * so that we can continue refcounting correctly.
	 */
	if (dev->vblank != NULL) {
		mtx_enter(&dev->vblank->vb_lock);
		for (i = 0; i < dev->vblank->vb_num; i++) {
			wakeup(&dev->vblank->vb_crtcs[i]);
			dev->vblank->vb_crtcs[i].vbl_enabled = 0;
			dev->vblank->vb_crtcs[i].vbl_last =
			    dev->driver->get_vblank_counter(dev, i);
		}
		mtx_leave(&dev->vblank->vb_lock);
	}

	DRM_DEBUG("irq=%d\n", dev->irq);

	dev->driver->irq_uninstall(dev);

	return (0);
}

int
drm_control(struct drm_device *dev, void *data, struct drm_file *file_priv)
{
	struct drm_control	*ctl = data;

	/* Handle drivers who used to require IRQ setup no longer does. */
	if (!(dev->driver->flags & DRIVER_IRQ))
		return (0);

	switch (ctl->func) {
	case DRM_INST_HANDLER:
		if (dev->if_version < DRM_IF_VERSION(1, 2) &&
		    ctl->irq != dev->irq)
			return (EINVAL);
		return (drm_irq_install(dev));
	case DRM_UNINST_HANDLER:
		return (drm_irq_uninstall(dev));
	default:
		return (EINVAL);
	}
}

void
vblank_disable(void *arg)
{
	struct drm_device	*dev = (struct drm_device*)arg;
	struct drm_vblank_info	*vbl = dev->vblank;
	struct drm_vblank	*crtc;
	int			 i;

	mtx_enter(&vbl->vb_lock);
	for (i = 0; i < vbl->vb_num; i++) {
		crtc = &vbl->vb_crtcs[i];

		if (crtc->vbl_refs == 0 && crtc->vbl_enabled) {
			DPRINTF("%s: disabling crtc %d\n", __func__, i);
			crtc->vbl_last =
			    dev->driver->get_vblank_counter(dev, i);
			dev->driver->disable_vblank(dev, i);
			crtc->vbl_enabled = 0;
		}
	}
	mtx_leave(&vbl->vb_lock);
}

void
drm_vblank_cleanup(struct drm_device *dev)
{
	if (dev->vblank == NULL)
		return; /* not initialised */

	timeout_del(&dev->vblank->vb_disable_timer);

	vblank_disable(dev);

	drm_free(dev->vblank);
	dev->vblank = NULL;
}

int
drm_vblank_init(struct drm_device *dev, int num_crtcs)
{
	dev->vblank = malloc(sizeof(*dev->vblank) + (num_crtcs *
	    sizeof(struct drm_vblank)), M_DRM,  M_WAITOK | M_CANFAIL | M_ZERO);
	if (dev->vblank == NULL)
		return (ENOMEM);

	dev->vblank->vb_num = num_crtcs;
	mtx_init(&dev->vblank->vb_lock, IPL_TTY);
	timeout_set(&dev->vblank->vb_disable_timer, vblank_disable, dev);

	return (0);
}

u_int32_t
drm_vblank_count(struct drm_device *dev, int crtc)
{
	return (dev->vblank->vb_crtcs[crtc].vbl_count);
}

void
drm_update_vblank_count(struct drm_device *dev, int crtc)
{
	u_int32_t	cur_vblank, diff;

	/*
	 * Interrupt was disabled prior to this call, so deal with counter wrap
	 * note that we may have lost a full vb_max events if
	 * the register is small or the interrupts were off for a long time.
	 */
	cur_vblank = dev->driver->get_vblank_counter(dev, crtc);
	diff = cur_vblank - dev->vblank->vb_crtcs[crtc].vbl_last;
	if (cur_vblank < dev->vblank->vb_crtcs[crtc].vbl_last)
		diff += dev->vblank->vb_max;

	dev->vblank->vb_crtcs[crtc].vbl_count += diff;
}

int
drm_vblank_get(struct drm_device *dev, int crtc)
{
	struct drm_vblank_info	*vbl = dev->vblank;
	int			 ret = 0;

	if (dev->irq_enabled == 0)
		return (EINVAL);

	mtx_enter(&vbl->vb_lock);
	DPRINTF("%s: %d refs = %d\n", __func__, crtc,
	    vbl->vb_crtcs[crtc].vbl_refs);
	vbl->vb_crtcs[crtc].vbl_refs++;
	if (vbl->vb_crtcs[crtc].vbl_refs == 1 &&
	    vbl->vb_crtcs[crtc].vbl_enabled == 0) {
		if ((ret = dev->driver->enable_vblank(dev, crtc)) == 0) {
			vbl->vb_crtcs[crtc].vbl_enabled = 1;
			drm_update_vblank_count(dev, crtc);
		} else {
			vbl->vb_crtcs[crtc].vbl_refs--;
		}

	}
	mtx_leave(&vbl->vb_lock);

	return (ret);
}

void
drm_vblank_put(struct drm_device *dev, int crtc)
{
	mtx_enter(&dev->vblank->vb_lock);
	/* Last user schedules disable */
	DPRINTF("%s: %d  refs = %d\n", __func__, crtc,
	    dev->vblank->vb_crtcs[crtc].vbl_refs);
	KASSERT(dev->vblank->vb_crtcs[crtc].vbl_refs > 0);
	if (--dev->vblank->vb_crtcs[crtc].vbl_refs == 0)
		timeout_add_sec(&dev->vblank->vb_disable_timer, 5);
	mtx_leave(&dev->vblank->vb_lock);
}

int
drm_modeset_ctl(struct drm_device *dev, void *data, struct drm_file *file_priv)
{
	struct drm_modeset_ctl	*modeset = data;
	struct drm_vblank	*vbl;
	int			 crtc, ret = 0;

	/* not initialised yet, just noop */
	if (dev->vblank == NULL)
		return (0);

	crtc = modeset->crtc;
	if (crtc >= dev->vblank->vb_num)
		return (EINVAL);

	vbl = &dev->vblank->vb_crtcs[crtc];

	/*
	 * If interrupts are enabled/disabled between calls to this ioctl then
	 * it can get nasty. So just grab a reference so that the interrupts
	 * keep going through the modeset
	 */
	switch (modeset->cmd) {
	case _DRM_PRE_MODESET:
		DPRINTF("%s: pre modeset on %d\n", __func__, crtc);
		if (vbl->vbl_inmodeset == 0) {
			mtx_enter(&dev->vblank->vb_lock);
			vbl->vbl_inmodeset = 0x1;
			mtx_leave(&dev->vblank->vb_lock);
			if (drm_vblank_get(dev, crtc) == 0)
				vbl->vbl_inmodeset |= 0x2;
		}
		break;
	case _DRM_POST_MODESET:
		DPRINTF("%s: post modeset on %d\n", __func__, crtc);
		if (vbl->vbl_inmodeset) {
			if (vbl->vbl_inmodeset & 0x2)
				drm_vblank_put(dev, crtc);
			vbl->vbl_inmodeset = 0;
		}
		break;
	default:
		ret = EINVAL;
		break;
	}

	return (ret);
}

int
drm_wait_vblank(struct drm_device *dev, void *data, struct drm_file *file_priv)
{
	struct timeval		 now;
	union drm_wait_vblank	*vblwait = data;
	int			 ret, flags, crtc, seq;

	if (!dev->irq_enabled || dev->vblank == NULL ||
	    vblwait->request.type & _DRM_VBLANK_SIGNAL)
		return (EINVAL);

	flags = vblwait->request.type & _DRM_VBLANK_FLAGS_MASK;
	crtc = flags & _DRM_VBLANK_SECONDARY ? 1 : 0;

	if (crtc >= dev->vblank->vb_num)
		return (EINVAL);

	if ((ret = drm_vblank_get(dev, crtc)) != 0)
		return (ret);
	seq = drm_vblank_count(dev,crtc);

	if (vblwait->request.type & _DRM_VBLANK_RELATIVE) {
		vblwait->request.sequence += seq;
		vblwait->request.type &= ~_DRM_VBLANK_RELATIVE;
	}

	flags = vblwait->request.type & _DRM_VBLANK_FLAGS_MASK;
	if ((flags & _DRM_VBLANK_NEXTONMISS) &&
	    (seq - vblwait->request.sequence) <= (1<<23)) {
		vblwait->request.sequence = seq + 1;
	}

	DPRINTF("%s: %d waiting on %d, current %d\n", __func__, crtc,
	     vblwait->request.sequence, drm_vblank_count(dev, crtc));
	DRM_WAIT_ON(ret, &dev->vblank->vb_crtcs[crtc], &dev->vblank->vb_lock,
	    3 * hz, "drmvblq", ((drm_vblank_count(dev, crtc) -
	    vblwait->request.sequence) <= (1 << 23)) || dev->irq_enabled == 0);

	microtime(&now);
	vblwait->reply.tval_sec = now.tv_sec;
	vblwait->reply.tval_usec = now.tv_usec;
	vblwait->reply.sequence = drm_vblank_count(dev, crtc);
	DPRINTF("%s: %d done waiting, seq = %d\n", __func__, crtc,
	    vblwait->reply.sequence);

	drm_vblank_put(dev, crtc);
	return (ret);
}

void
drm_handle_vblank(struct drm_device *dev, int crtc)
{
	/*
	 * XXX if we had proper atomic operations this mutex wouldn't
	 * XXX need to be held.
	 */
	mtx_enter(&dev->vblank->vb_lock);
	dev->vblank->vb_crtcs[crtc].vbl_count++;
	wakeup(&dev->vblank->vb_crtcs[crtc]);
	mtx_leave(&dev->vblank->vb_lock);
}
