/*
 * misc x11 functions:  pixmap handling (incl. MIT SHMEM), event
 *                      tracking for the TV widget.
 *
 *  (c) 1998 Gerd Knorr <kraxel@goldbach.in-berlin.de>
 *
 */

#include "config.h"

#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <errno.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/time.h>
#include <sys/socket.h>
#include <sys/ipc.h>
#include <sys/shm.h>

#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/Intrinsic.h>
#include <X11/StringDefs.h>
#include <X11/Shell.h>
#include <X11/extensions/XShm.h>
#ifdef HAVE_LIBXV
# include <X11/extensions/Xv.h>
# include <X11/extensions/Xvlib.h>
#endif

#include "grab-ng.h"
#include "capture.h"
#include "channel.h"
#include "x11.h"
#include "xv.h"
#include "commands.h"
#include "blit.h"

#define DEL_TIMER(proc)     XtRemoveTimeOut(proc)
#define ADD_TIMER(proc)     XtAppAddTimeOut(app_context,200,proc,NULL)

extern XtAppContext    app_context;
extern XVisualInfo     vinfo;

/* ------------------------------------------------------------------------ */

Pixmap
x11_capture_pixmap(Display *dpy, XVisualInfo *vinfo, Colormap colormap,
		   unsigned int width, unsigned int height)
{
    struct ng_video_buf *buf;
    struct ng_video_fmt fmt;
    Pixmap pix = 0;

    if (!(f_drv & CAN_CAPTURE))
	return 0;

    memset(&fmt,0,sizeof(fmt));
    fmt.fmtid  = x11_dpy_fmtid;
    fmt.width  = width  ? width  : cur_tv_width;
    fmt.height = height ? height : cur_tv_height;
    if (NULL == (buf = ng_grabber_get_image(&fmt)))
	return 0;
    buf = ng_filter_single(cur_filter,buf);
    pix = x11_create_pixmap(dpy,vinfo,buf);
    ng_release_video_buf(buf);
    return pix;
}

void
x11_label_pixmap(Display *dpy, Colormap colormap, Pixmap pixmap,
		 int height, char *label)
{
    static XFontStruct    *font;
    static XColor          color,dummy;
    XGCValues              values;
    GC                     gc;
    
    if (!font) {
	font = XLoadQueryFont(dpy,"fixed");
	XAllocNamedColor(dpy,colormap,"yellow",&color,&dummy);
    }
    values.font       = font->fid;
    values.foreground = color.pixel;
    gc = XCreateGC(dpy, pixmap, GCFont | GCForeground, &values);
    XDrawString(dpy,pixmap,gc,5,height-5,label,strlen(label));
    XFreeGC(dpy, gc);
}

static int
x11_error_dev_null(Display * dpy, XErrorEvent * event)
{
    return 0;
}

/* ------------------------------------------------------------------------ */
/* video grabdisplay stuff                                                  */

struct video_handle {
    Widget                win;
    Dimension             width,height;
    XtWorkProcId          work_id;
    int                   suspend;         /* temporarely disabled */
    int                   nw,nh;           /* new size (suspend)   */
    struct ng_video_fmt   best;
    struct blit_state     *blit;

    /* image filtering */
    struct ng_filter      *filter;
    void                  *fhandle;
    struct ng_video_fmt   ffmt;
};
struct video_handle vh;

void video_gd_init(Widget widget, int use_gl)
{
    struct video_handle *h = &vh;

    if (debug)
	fprintf(stderr,"gd: init\n");
    h->win  = widget;
    h->blit = blit_init(h->win,&vinfo, use_gl);
}

static struct ng_video_buf*
video_gd_filter(struct video_handle *h, struct ng_video_buf *buf)
{
    if (NULL != h->filter &&
	(cur_filter      != h->filter ||
	 buf->fmt.fmtid  != h->ffmt.fmtid ||
	 buf->fmt.width  != h->ffmt.width ||
	 buf->fmt.height != h->ffmt.height)) {
	h->filter->fini(h->fhandle);
	h->filter  = NULL;
	h->fhandle = NULL;
	memset(&h->ffmt,0,sizeof(h->ffmt));
    }
    if ((1 << buf->fmt.fmtid) & cur_filter->fmts) {
	if (NULL == h->filter) {
	    h->filter  = cur_filter;
	    h->fhandle = h->filter->init(&buf->fmt);
	    h->ffmt    = buf->fmt;
	}
	buf = cur_filter->frame(h->fhandle,buf);
    }
    return buf;
}

int
video_gd_blitframe(struct video_handle *h, struct ng_video_buf *buf)
{
    if (buf->fmt.width  > cur_tv_width ||
	buf->fmt.height > cur_tv_height)
	return -1;

    if (cur_filter)
	buf = video_gd_filter(h,buf);
    blit_putframe(h->blit,buf);
    return 0;
}

static Boolean
video_gd_idle(XtPointer data)
{
    struct video_handle *h = data;
    struct ng_video_buf *buf;
    
    if (!(f_drv & CAN_CAPTURE))
	goto oops;

    buf = ng_grabber_grab_image(0);
    if (NULL != buf) {
	video_gd_blitframe(h,buf);
    } else {
	goto oops;
    }
    
    if (debug) {
	static long count,lastsec;
	struct timeval  t;
	struct timezone tz;
	gettimeofday(&t,&tz);
	if (t.tv_sec != lastsec) {
	    if (lastsec == t.tv_sec-1)
		fprintf(stderr,"%5ld fps \r", count);
	    lastsec = t.tv_sec;
	    count = 0;
	}
	count++;
    }
    return FALSE;

 oops:
    h->work_id = 0;
    if (f_drv & CAN_CAPTURE)
	drv->stopvideo(h_drv);
    return TRUE;
}

void
video_gd_start(void)
{
    struct video_handle *h = &vh;

    if (debug)
	fprintf(stderr,"gd: start [%d]\n",h->best.fmtid);
    if (0 == h->best.fmtid)
	return;
    if (0 != ng_grabber_setformat(&h->best,0))
	return;
    drv->startvideo(h_drv,-1,2);
//    drv->startvideo(h_drv,-1,4);
    h->work_id = XtAppAddWorkProc(app_context, video_gd_idle, h);
}

void
video_gd_stop(void)
{
    struct video_handle *h = &vh;

    if (debug)
	fprintf(stderr,"gd: stop\n");
    if (h->work_id) {
	drv->stopvideo(h_drv);
	XtRemoveWorkProc(h->work_id);
	h->work_id = 0;
	blit_fini_frame(h->blit);
    }
}

void
video_gd_suspend(void)
{
    struct video_handle *h = &vh;
    
    h->suspend = 1;
    if (cur_capture != CAPTURE_GRABDISPLAY)
	return;
    do_va_cmd(2, "capture", "off");
}

void
video_gd_restart(void)
{
    struct video_handle *h = &vh;

    if (!h->suspend)
	return;
    h->suspend = 0;
    if (h->nw && h->nh) {
	video_gd_configure(h->nw,h->nh);
	h->nw = 0;
	h->nh = 0;
    }
    if (cur_capture != CAPTURE_OFF)
	return;
    do_va_cmd(2, "capture", "grab");
}

void
video_gd_configure(int width, int height)
{
    struct video_handle *h = &vh;
    unsigned int i,fmtids[2*VIDEO_FMT_COUNT];

    if (!(f_drv & CAN_CAPTURE))
	return;

    blit_resize(h->blit,width,height);

    if (h->suspend) {
	if (debug)
	    fprintf(stderr,"gd: delay configure\n");
	h->nw = width;
	h->nh = height;
	return;
    }

    if (debug)
	fprintf(stderr,"gd: config %dx%d win=%lx\n",
		width,height,XtWindow(h->win));
	
    if (!XtWindow(h->win))
	return;

    cur_tv_width  = width;
    cur_tv_height = height;
    h->best.width   = width;
    h->best.height  = height;
    h->best.bytesperline = 0;
    ng_ratio_fixup(&cur_tv_width,  &cur_tv_height,  NULL, NULL);
    ng_ratio_fixup(&h->best.width, &h->best.height, NULL, NULL);

    if (0 == h->best.fmtid) {
	blit_get_formats(h->blit,fmtids,sizeof(fmtids)/sizeof(int));
	for (i = 0; i < sizeof(fmtids)/sizeof(int); i++) {
	    h->best.fmtid = fmtids[i];
	    if (0 == ng_grabber_setformat(&h->best,0))
		goto done;
	}
	/* failed */
	h->best.fmtid = 0;
    }

 done:
    if (debug)
	fprintf(stderr,"grabdisplay: using \"%s\"\n",
		ng_vfmt_to_desc[h->best.fmtid]);
    if (cur_capture == CAPTURE_GRABDISPLAY) {
	do_va_cmd(2, "capture", "off");
	do_va_cmd(2, "capture", "grab");
    }
}

/* ------------------------------------------------------------------------ */
/* video overlay stuff                                                      */

unsigned int     swidth,sheight;           /* screen  */
static int       x11_overlay_fmtid;

/* window  */
static Widget    video,video_parent;
static int       wx, wy, wmap;
static struct ng_video_fmt wfmt;

static XtIntervalId          overlay_refresh;
static int                   did_refresh, oc_count;
static int                   visibility = VisibilityFullyObscured;
static int                   conf = 1, move = 1;
static int                   overlay_on = 0, overlay_enabled = 0;
static struct OVERLAY_CLIP   oc[256];
static XtWorkProcId          conf_id;

/* ------------------------------------------------------------------------ */

char *event_names[] = {
    "0", "1",
    "KeyPress",
    "KeyRelease",
    "ButtonPress",
    "ButtonRelease",
    "MotionNotify",
    "EnterNotify",
    "LeaveNotify",
    "FocusIn",
    "FocusOut",
    "KeymapNotify",
    "Expose",
    "GraphicsExpose",
    "NoExpose",
    "VisibilityNotify",
    "CreateNotify",
    "DestroyNotify",
    "UnmapNotify",
    "MapNotify",
    "MapRequest",
    "ReparentNotify",
    "ConfigureNotify",
    "ConfigureRequest",
    "GravityNotify",
    "ResizeRequest",
    "CirculateNotify",
    "CirculateRequest",
    "PropertyNotify",
    "SelectionClear",
    "SelectionRequest",
    "SelectionNotify",
    "ColormapNotify",
    "ClientMessage",
    "MappingNotify"
};
const int nevent_names = sizeof(event_names)/sizeof(event_names[0]);

/* ------------------------------------------------------------------------ */

static void
add_clip(int x1, int y1, int x2, int y2)
{
    if (oc[oc_count].x1 != x1 || oc[oc_count].y1 != y1 ||
	oc[oc_count].x2 != x2 || oc[oc_count].y2 != y2) {
	conf = 1;
    }
    oc[oc_count].x1 = x1;
    oc[oc_count].y1 = y1;
    oc[oc_count].x2 = x2;
    oc[oc_count].y2 = y2;
    oc_count++;
} 

static void
get_clips(void)
{
    int x1,y1,x2,y2,lastcount;
    Display *dpy;
    XWindowAttributes wts;
    Window root, me, rroot, parent, *children;
    uint nchildren, i;
    void *old_handler = XSetErrorHandler(x11_error_dev_null);

    if (debug > 1)
	fprintf(stderr," getclips");
    lastcount = oc_count;
    oc_count = 0;
    dpy = XtDisplay(video);

    if (wx<0)
	add_clip(0, 0, (uint)(-wx), wfmt.height);
    if (wy<0)
	add_clip(0, 0, wfmt.width, (uint)(-wy));
    if ((wx+wfmt.width) > swidth)
	add_clip(swidth-wx, 0, wfmt.width, wfmt.height);
    if ((wy+wfmt.height) > sheight)
	add_clip(0, sheight-wy, wfmt.width, wfmt.height);
    
    root=DefaultRootWindow(dpy);
    me = XtWindow(video);
    for (;;) {
	XQueryTree(dpy, me, &rroot, &parent, &children, &nchildren);
	XFree((char *) children);
	/* fprintf(stderr,"me=0x%x, parent=0x%x\n",me,parent); */
	if (root == parent)
	    break;
	me = parent;
    }
    XQueryTree(dpy, root, &rroot, &parent, &children, &nchildren);

    for (i = 0; i < nchildren; i++)
	if (children[i]==me)
	    break;
    
    for (i++; i<nchildren; i++) {
	XGetWindowAttributes(dpy, children[i], &wts);
	if (!(wts.map_state & IsViewable))
	    continue;

	x1=wts.x-wx;
	y1=wts.y-wy;
	x2=x1+wts.width+2*wts.border_width;
	y2=y1+wts.height+2*wts.border_width;
	if ((x2 < 0) || (x1 > (int)wfmt.width) ||
	    (y2 < 0) || (y1 > (int)wfmt.height))
	    continue;
	
	if (x1<0)      	         x1=0;
	if (y1<0)                y1=0;
	if (x2>(int)wfmt.width)  x2=wfmt.width;
	if (y2>(int)wfmt.height) y2=wfmt.height;
	add_clip(x1, y1, x2, y2);
    }
    XFree((char *) children);

    if (lastcount != oc_count)
	conf = 1;
    XSetErrorHandler(old_handler);
}

static void
refresh_timer(XtPointer clientData, XtIntervalId *id)
{
    Window   win = RootWindowOfScreen(XtScreen(video));
    Display *dpy = XtDisplay(video);
    XSetWindowAttributes xswa;
    unsigned long mask;
    Window   tmp;

    if (!move && wmap && visibility == VisibilityUnobscured) {
	if (debug > 1)
	    fprintf(stderr,"video: refresh skipped\n");
	return;
    }

    if (debug > 1)
	fprintf(stderr,"video: refresh\n");
    overlay_refresh = 0;
    if (wmap && visibility != VisibilityFullyObscured)
	did_refresh = 1;

    xswa.override_redirect = True;
    xswa.backing_store = NotUseful;
    xswa.save_under = False;
    mask = (CWSaveUnder | CWBackingStore| CWOverrideRedirect );
    tmp = XCreateWindow(dpy,win, 0,0, swidth,sheight, 0,
			CopyFromParent, InputOutput, CopyFromParent,
			mask, &xswa);
    XMapWindow(dpy, tmp);
    XUnmapWindow(dpy, tmp);
    XDestroyWindow(dpy, tmp);
    move = 0;
}

static Boolean
configure_delayed(XtPointer data)
{
    if (debug > 1)
	fprintf(stderr,"video: configure delayed");
    if (wmap && visibility != VisibilityFullyObscured) {
	if (visibility == VisibilityPartiallyObscured)
	    get_clips();
	else
	    oc_count = 0;

	if (debug > 1)
	    fprintf(stderr," %s\n",conf ? "yes" : "no");
	if (conf) {
	    overlay_on = 1;
	    if (f_drv & CAN_OVERLAY)
		drv->overlay(h_drv,&wfmt,wx,wy,oc,oc_count,1);
	    if (overlay_refresh)
		DEL_TIMER(overlay_refresh);
	    overlay_refresh = ADD_TIMER(refresh_timer);
	    conf = 0;
	}
    } else {
	if (debug > 1)
	    fprintf(stderr," off\n");
	if (conf && overlay_on) {
	    overlay_on = 0;
	    if (f_drv & CAN_OVERLAY)
		drv->overlay(h_drv,NULL,0,0,NULL,0,0);
	    if (overlay_refresh)
		DEL_TIMER(overlay_refresh);
	    overlay_refresh = ADD_TIMER(refresh_timer);
	    conf = 0;
	}
    }
    conf_id = 0;
    return TRUE;
}

static void
configure_overlay(void)
{
    if (!overlay_enabled)
	return;

#ifdef HAVE_LIBXV
    if (have_xv) {
	if (wfmt.width && wfmt.height)
	    xv_video(XtWindow(video),wfmt.width,wfmt.height,1);
	return;
    }
#endif

    if (0 == conf_id)
	conf_id = XtAppAddWorkProc(app_context,configure_delayed,NULL);
}

void
video_new_size()
{
    Dimension x,y,w,h;

    XtVaGetValues(video_parent, XtNx, &x, XtNy, &y,
		  XtNwidth, &w, XtNheight, &h, NULL);
    wx          = x; if (wx > 32768)          wx          -= 65536;
    wy          = y; if (wy > 32768)          wy          -= 65536;
    wfmt.width  = w; if (wfmt.width > 32768)  wfmt.width  -= 65536;
    wfmt.height = h; if (wfmt.height > 32768) wfmt.height -= 65536;
    wfmt.fmtid  = x11_overlay_fmtid;
    if (debug > 1)
	fprintf(stderr,"video: shell: size %dx%d+%d+%d\n",
		wfmt.width,wfmt.height,wx,wy);

    conf = 1;
    move = 1;
    configure_overlay();
}

/* ------------------------------------------------------------------------ */

static void
video_event(Widget widget, XtPointer client_data, XEvent *event, Boolean *d)
{
    if (widget == video_parent) {
	/* shell widget */
	switch(event->type) {
	case ConfigureNotify:
#if 0
	    wx      = event->xconfigure.x;
	    wy      = event->xconfigure.y;
	    wwidth  = event->xconfigure.width;
	    wheight = event->xconfigure.height;
	    if (debug > 1)
		fprintf(stderr,"video: shell: cfg %dx%d+%d+%d\n",
			wwidth,wheight,wx,wy);
#endif
	    video_new_size();
	    break;
	case MapNotify:
	    if (debug > 1)
		fprintf(stderr,"video: shell: map\n");
	    wmap = 1;
	    conf = 1;
	    configure_overlay();
	    break;
	case UnmapNotify:
	    if (debug > 1)
		fprintf(stderr,"video: shell: unmap\n");
	    wmap = 0;
	    conf = 1;
	    configure_overlay();
	    break;
	default:
	    if (debug > 1)
		fprintf(stderr,"video: shell: %s\n",
			event_names[event->type]);
	}
	return;

    } else {
	/* TV widget (+root window) */
	switch(event->type) {
	case Expose:
	    if (event->xvisibility.window == XtWindow(video)) {
		/* tv */
		if (!event->xexpose.count) {
		    if (did_refresh) {
			did_refresh = 0;
			if (debug > 1)
			    fprintf(stderr,"video: tv: last refresh expose\n");
		    } else {
			if (debug > 1)
			    fprintf(stderr,"video: tv: expose\n");
			conf = 1;
			configure_overlay();
		    }
		}
	    }
	    break;
	case VisibilityNotify:
	    if (event->xvisibility.window == XtWindow(video)) {
		/* tv */
		visibility = event->xvisibility.state;
		if (debug > 1)
		    fprintf(stderr,"video: tv: visibility %d%s\n",
			    event->xvisibility.state,
			    did_refresh?" (ignored)":"");
		if (did_refresh) {
		    if (event->xvisibility.state != VisibilityFullyObscured)
			did_refresh = 0;
		} else {
		    conf = 1;
		    configure_overlay();
		}
	    } else {
		/* root */
		if (debug > 1)
		    fprintf(stderr,"video: root: visibility\n");
	    }
	    break;
	case MapNotify:
	case UnmapNotify:
	case ConfigureNotify:
	    if (event->xvisibility.window != XtWindow(video)) {
		if (debug > 1)
		    fprintf(stderr,"video: root: %s%s\n",
			    event_names[event->type],did_refresh?" (ignored)":"");
		if (!did_refresh)
		    configure_overlay();
	    }
	    break;
	default:
	    if (debug > 1)
		fprintf(stderr,"video: tv(+root): %s\n",
			event_names[event->type]);
	    break;
	}	
    }
}

void
video_overlay(int state)
{
    if (state) {
	conf = 1;
	overlay_enabled = 1;
	configure_overlay();
    } else {
	if (1 == overlay_enabled) {
#ifdef HAVE_LIBXV
	    if (have_xv) {
		xv_video(XtWindow(video),0,0,0);
	    } else
#endif
	    {
		overlay_on = 0;
		if (f_drv & CAN_OVERLAY)
		    drv->overlay(h_drv,NULL,0,0,NULL,0,0);
		overlay_refresh = ADD_TIMER(refresh_timer);
	    }
	}
	overlay_enabled = 0;
    }
}

Widget
video_init(Widget parent, XVisualInfo *vinfo, WidgetClass class,
	   int args_bpp, int args_gl)
{
    Window root = DefaultRootWindow(XtDisplay(parent));

    swidth  = XtScreen(parent)->width;
    sheight = XtScreen(parent)->height;

    x11_overlay_fmtid = x11_dpy_fmtid;
    if (ImageByteOrder(XtDisplay(parent)) == MSBFirst) {
	/* X-Server is BE */
	switch(args_bpp) {
	case  8: x11_overlay_fmtid = VIDEO_RGB08;    break;
	case 15: x11_overlay_fmtid = VIDEO_RGB15_BE; break;
	case 16: x11_overlay_fmtid = VIDEO_RGB16_BE; break;
	case 24: x11_overlay_fmtid = VIDEO_BGR24;    break;
	case 32: x11_overlay_fmtid = VIDEO_BGR32;    break;
	}
    } else {
	/* X-Server is LE */
	switch(args_bpp) {
	case  8: x11_overlay_fmtid = VIDEO_RGB08;    break;
	case 15: x11_overlay_fmtid = VIDEO_RGB15_LE; break;
	case 16: x11_overlay_fmtid = VIDEO_RGB16_LE; break;
	case 24: x11_overlay_fmtid = VIDEO_BGR24;    break;
	case 32: x11_overlay_fmtid = VIDEO_BGR32;    break;
	}
    }

    video_parent = parent;
    video = XtVaCreateManagedWidget("tv",class,parent,NULL);

    /* Shell widget -- need map, unmap, configure */
    XtAddEventHandler(parent,
		      StructureNotifyMask,
		      True, video_event, NULL);

    if (!have_xv) {
	/* TV Widget -- need visibility, expose */
	XtAddEventHandler(video,
			  VisibilityChangeMask |
			  StructureNotifyMask,
			  False, video_event, NULL);
	
	/* root window -- need */
	XSelectInput(XtDisplay(video),root,
		     VisibilityChangeMask |
		     SubstructureNotifyMask |
		     StructureNotifyMask);
	
	XtRegisterDrawable(XtDisplay(video),root,video);
    }

    return video;
}

void
video_close(void)
{
    Window root = DefaultRootWindow(XtDisplay(video));

    if (overlay_refresh)
	DEL_TIMER(overlay_refresh);
    XSelectInput(XtDisplay(video),root,0);
    XtUnregisterDrawable(XtDisplay(video),root);
}
