/*
 * Compiz motion blur effect plugin
 *
 * mblur.c
 *
 * Copyright : (C) 2007 by Dennis Kasprzyk
 * E-mail    : onestone@beryl-project.org
 *
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 */

#include <math.h>

#include <compiz-core.h>

#include "mblur_options.h"

#define GET_MBLUR_DISPLAY(d)                                  \
    ((MblurDisplay *) (d)->base.privates[displayPrivateIndex].ptr)

#define MBLUR_DISPLAY(d)                                      \
    MblurDisplay *md = GET_MBLUR_DISPLAY (d)

#define GET_MBLUR_SCREEN(s, md)                               \
    ((MblurScreen *) (s)->base.privates[(md)->screenPrivateIndex].ptr)

#define MBLUR_SCREEN(s)                                       \
    MblurScreen *ms = GET_MBLUR_SCREEN (s, GET_MBLUR_DISPLAY (s->display))

static int displayPrivateIndex = 0;

typedef struct _MblurDisplay
{
    int screenPrivateIndex;
}
MblurDisplay;

typedef struct _MblurScreen
{
    /* functions that we will intercept */
    PreparePaintScreenProc     preparePaintScreen;
    PaintScreenProc            paintScreen;
    PaintTransformedOutputProc paintTransformedOutput;

    Bool active;
    Bool update; /* is an update of the motion blut texture needed */
    
    float alpha; /* motion blur blending value */
    float timer; /* motion blur fadeout time */
    Bool activated;

    GLuint texture;
}
MblurScreen;

/* activate/deactivate motion blur */

static Bool
mblurToggle (CompDisplay     *d,
	     CompAction      *ac,
	     CompActionState state,
	     CompOption      *option,
	     int             nOption)
{
    CompScreen *s;

    s = findScreenAtDisplay (d, getIntOptionNamed (option, nOption, "root", 0));

    if (s)
    {
	MBLUR_SCREEN (s);
	ms->activated = !ms->activated;
    }

    return FALSE;
}

static void
mblurPreparePaintScreen (CompScreen * s,
			 int msec)
{
    MBLUR_SCREEN (s);

    ms->active |= ms->activated;

    /* fade motion blur out if no longer active */

    if (ms->activated)
    {
	ms->timer = 500;
    }
    else
    {
	ms->timer -= msec;
    }

    // calculate motion blur strength dependent on framerate
    float val   = 101 - MIN (100, MAX (1, msec) );
    float a_val = mblurGetStrength (s) / 20.0;

    a_val = a_val * a_val;
    a_val /= 100.0;

    ms->alpha = 1.0 - pow (a_val, 1.0 / val);

    if (ms->active && ms->timer <= 0)
	damageScreen (s);

    if (ms->timer <= 0)
    {
	ms->active = FALSE;
    }

    if (ms->update && ms->active)
	damageScreen (s);

    UNWRAP (ms, s, preparePaintScreen);
    (*s->preparePaintScreen) (s, msec);
    WRAP (ms, s, preparePaintScreen, mblurPreparePaintScreen);
}


static void
mblurPaintScreen (CompScreen   *s,
		  CompOutput   *outputs,
		  int          numOutput,
		  unsigned int mask)
{

    MBLUR_SCREEN (s);

    if (!ms->active)
	ms->update = TRUE;


    UNWRAP (ms, s, paintScreen);
    (*s->paintScreen) (s, outputs, numOutput, mask);
    WRAP (ms, s, paintScreen, mblurPaintScreen);

    Bool enable_scissor = FALSE;

    if (ms->active && glIsEnabled (GL_SCISSOR_TEST) )
    {
	glDisable (GL_SCISSOR_TEST);
	enable_scissor = TRUE;
    }

    if (ms->active && mblurGetMode (s) == ModeTextureCopy)
    {

	float tx, ty;
	GLuint target;

	if (s->textureNonPowerOfTwo ||
	    (POWER_OF_TWO (s->width) && POWER_OF_TWO (s->height) ) )
	{
	    target = GL_TEXTURE_2D;
	    tx = 1.0f / s->width;
	    ty = 1.0f / s->height;
	}
	else
	{
	    target = GL_TEXTURE_RECTANGLE_NV;
	    tx = 1;
	    ty = 1;
	}


	if (!ms->texture)
	{
	    glGenTextures (1, &ms->texture);
	    glBindTexture (target, ms->texture);

	    glTexParameteri (target, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	    glTexParameteri (target, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

	    glTexParameteri (target, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
	    glTexParameteri (target, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

	    glBindTexture (target, 0);
	}

	// blend motion blur texture to screen
	glPushAttrib (GL_COLOR_BUFFER_BIT | GL_TEXTURE_BIT | GL_VIEWPORT_BIT);
	glPushMatrix ();
	glLoadIdentity ();

	glViewport (0, 0, s->width, s->height);
	glTranslatef (-0.5f, -0.5f, -DEFAULT_Z_CAMERA);
	glScalef (1.0f / s->width, -1.0f / s->height, 1.0f);
	glTranslatef (0.0f, -s->height, 0.0f);
	glBindTexture (target, ms->texture);
	glEnable (target);

	if (!ms->update)
	{
	    glEnable (GL_BLEND);

	    glBlendFunc (GL_ONE_MINUS_SRC_ALPHA, GL_SRC_ALPHA);
	    ms->alpha = (ms->timer / 500.0) *
			ms->alpha + (1.0 - (ms->timer / 500.0) ) * 0.5;

	    glColor4f (1, 1, 1, ms->alpha);

	    glTexEnvf (GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);

	    glBegin (GL_QUADS);
	    glTexCoord2f (0, s->height * ty);
	    glVertex2f (0, 0);
	    glTexCoord2f (0, 0);
	    glVertex2f (0, s->height);
	    glTexCoord2f (s->width * tx, 0);
	    glVertex2f (s->width, s->height);
	    glTexCoord2f (s->width * tx, s->height * ty);
	    glVertex2f (s->width, 0);
	    glEnd ();

	    glTexEnvf (GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);

	    glBlendFunc (GL_ONE, GL_ONE_MINUS_SRC_ALPHA);

	    glDisable (GL_BLEND);

	    // copy new screen to motion blur texture
	    glCopyTexSubImage2D (target, 0, 0, 0, 0, 0, s->width, s->height);
	}
	else
	{
	    glCopyTexImage2D (target, 0, GL_RGB, 0, 0,
			      s->width, s->height, 0);
	}

	glBindTexture (target, 0);

	glDisable (target);

	glPopMatrix ();
	glPopAttrib ();

	ms->update = FALSE;
	damageScreen (s);
    }

    if (ms->active && mblurGetMode (s) == ModeAccumulationBuffer)
    {

	// create motion blur effect using accumulation buffer
	ms->alpha = (ms->timer / 500.0) *
		    ms->alpha + (1.0 - (ms->timer / 500.0) ) * 0.5;

	if (ms->update)
	{
	    glAccum (GL_LOAD, 1.0);
	}
	else
	{
	    glAccum (GL_MULT, 1.0 - ms->alpha);
	    glAccum (GL_ACCUM, ms->alpha);
	    glAccum (GL_RETURN, 1.0);
	}

	ms->update = FALSE;

	damageScreen (s);
    }

    if (enable_scissor)
	glEnable (GL_SCISSOR_TEST);

}

static void
mblurPaintTransformedOutput (CompScreen              *s,
			     const ScreenPaintAttrib *sa,
			     const CompTransform     *transform,
			     Region                  region,
			     CompOutput              *output,
			     unsigned int            mask)
{

    MBLUR_SCREEN (s);

    if (mblurGetOnTransformedScreen (s) &&
	(mask & PAINT_SCREEN_TRANSFORMED_MASK) )
    {
	ms->active = TRUE;
	ms->timer = 500;
    }

    UNWRAP (ms, s, paintTransformedOutput);
    (*s->paintTransformedOutput) (s, sa, transform, region, output, mask);
    WRAP (ms, s, paintTransformedOutput, mblurPaintTransformedOutput);
}

static Bool
mblurInit (CompPlugin *p)
{
    displayPrivateIndex = allocateDisplayPrivateIndex ();

    if (displayPrivateIndex < 0)
	return FALSE;

    return TRUE;
}

static void
mblurFini (CompPlugin *p)
{
    if (displayPrivateIndex >= 0)
	freeDisplayPrivateIndex (displayPrivateIndex);
}

static Bool
mblurInitDisplay (CompPlugin  *p,
		  CompDisplay *d)
{
    MblurDisplay *md;

    if (!checkPluginABI ("core", CORE_ABIVERSION))
	return FALSE;

    /* Generate a blur display */
    md = (MblurDisplay *) calloc (1, sizeof (MblurDisplay));

    if (!md)
	return FALSE;

    /* Allocate a private index */
    md->screenPrivateIndex = allocateScreenPrivateIndex (d);

    /* Check if its valid */
    if (md->screenPrivateIndex < 0)
    {
	/* It's invalid so free memory and return */
	free (md);
	return FALSE;
    }

    /* Record the display */
    d->base.privates[displayPrivateIndex].ptr = md;

    mblurSetInitiateKeyInitiate (d, mblurToggle);

    return TRUE;
}

static void
mblurFiniDisplay (CompPlugin  *p,
		  CompDisplay *d)
{
    MBLUR_DISPLAY (d);

    /*Free the private index */
    freeScreenPrivateIndex (d, md->screenPrivateIndex);

    /*Free the pointer */
    free (md);
}

static Bool
mblurInitScreen (CompPlugin *p,
		 CompScreen *s)
{
    MBLUR_DISPLAY (s->display);

    /* Create a blur screen */
    MblurScreen *ms = (MblurScreen *) calloc (1, sizeof (MblurScreen) );

    s->base.privates[md->screenPrivateIndex].ptr = ms;

    ms->activated = FALSE;
    ms->update = TRUE;
    ms->texture = 0;

    /* Take over the window draw function */
    WRAP (ms, s, paintScreen, mblurPaintScreen);
    WRAP (ms, s, preparePaintScreen, mblurPreparePaintScreen);
    WRAP (ms, s, paintTransformedOutput, mblurPaintTransformedOutput);

    damageScreen (s);

    return TRUE;
}

static void
mblurFiniScreen (CompPlugin *p,
		 CompScreen *s)
{
    MBLUR_SCREEN (s);

    if (ms->texture)
	glDeleteTextures (1, &ms->texture);

    /* restore the original function */
    UNWRAP (ms, s, paintScreen);
    UNWRAP (ms, s, preparePaintScreen);
    UNWRAP (ms, s, paintTransformedOutput);

    /* free the screen pointer */
    free (ms);
}

static CompBool
mblurInitObject (CompPlugin *p,
		 CompObject *o)
{
    static InitPluginObjectProc dispTab[] = {
	(InitPluginObjectProc) 0,
	(InitPluginObjectProc) mblurInitDisplay,
	(InitPluginObjectProc) mblurInitScreen
    };

    RETURN_DISPATCH (o, dispTab, ARRAY_SIZE (dispTab), TRUE, (p, o));
}

static void
mblurFiniObject (CompPlugin *p,
		 CompObject *o)
{
    static FiniPluginObjectProc dispTab[] = {
    	(FiniPluginObjectProc) 0,
	(FiniPluginObjectProc) mblurFiniDisplay,
	(FiniPluginObjectProc) mblurFiniScreen
    };

    DISPATCH (o, dispTab, ARRAY_SIZE (dispTab), (p, o));
}

CompPluginVTable mblurVTable = {

    "mblur",
    0,
    mblurInit,
    mblurFini,
    mblurInitObject,
    mblurFiniObject,
    0,
    0
};

CompPluginVTable *
getCompPluginInfo (void)
{
    return &mblurVTable;
}
