/*	Direct3D OpenGL driver
 *
 * Copyright 2001-2002 TransGaming Technologies Inc.
 */
#include "config.h"

#include <assert.h>
#include <stdlib.h>
#include <math.h>

#include "winbase.h"
#include "winuser.h"
#include "wine/debug.h"
#include "ddrawi.h"
#include "d3d9.h"
#include "d3dhal.h"
#include "d3dgl_private.h"

WINE_DEFAULT_DEBUG_CHANNEL(ddraw);

extern int intercept_mode;

static inline void
GL_draw_vertices(GL_D3D_priv* priv, GLenum gltype, DWORD fvf,
		 DWORD start, DWORD count,
		 GL_IndexFunc index, LPVOID index_data,
		 DWORD ibase)
{
    if (priv->state.renderstate[D3DRENDERSTATE_LIGHTING] && (fvf & D3DFVF_NORMAL || priv->d3d_version >= 8))
	GL_draw_unlit_vertices(priv, gltype, fvf, start, count, index, index_data, ibase);
    else assert(FALSE);
}

static DWORD GL_DirectIndex(DWORD i, LPVOID p)
{
    return i;
}

static DWORD GL_IndexedIndex(DWORD i, LPVOID p)
{
    return ((LPWORD)p)[i];
}

static GLfloat* accum_feedback_rect(LPD3D_P d3dp, GLfloat *feedback, LPRECT rect, int v)
{
    int x1, y1, x2, y2;
    x1 = floorf(feedback[0]);
    x2 = ceilf(feedback[0]);
#ifndef HAVE_OSMESA
    y1 = d3dp->surf->lpGbl->wHeight - (int)ceilf(feedback[1]);
    y2 = d3dp->surf->lpGbl->wHeight - (int)floorf(feedback[1]);
#else
    y1 = floorf(feedback[1]);
    y2 = ceilf(feedback[1]);
#endif
    /* TRACE("  (%d,%d) <= (%f,%f)\n", x1, y1, feedback[0], feedback[1]); */
    if (!v) {
	rect->left = x1;
	rect->right = x2;
	rect->top = y1;
	rect->bottom = y2;
    } else {
	if (x1 < rect->left) rect->left = x1;
	if (x2 > rect->right) rect->right = x2;
	if (y1 < rect->top) rect->top = y1;
	if (y2 > rect->bottom) rect->bottom = y2;
    }
    return feedback+2;
}

static int calc_feedback_rect(GL_D3D_priv* priv, GLfloat *feedback, int count, LPRECT rect)
{
    LPD3D_P d3dp = priv->d3dp;
    int v = 0;
    GLfloat *end = feedback + count;
    while (feedback < end) {
	int n, tok = *feedback++;
	switch (tok) {
	case GL_POINT_TOKEN:
	    /* TRACE(" POINT\n"); */
	    feedback = accum_feedback_rect(d3dp, feedback, rect, v++);
	    break;
	case GL_LINE_TOKEN:
	case GL_LINE_RESET_TOKEN:
	    /* TRACE(" LINE\n"); */
	    feedback = accum_feedback_rect(d3dp, feedback, rect, v++);
	    feedback = accum_feedback_rect(d3dp, feedback, rect, v++);
	    break;
	case GL_POLYGON_TOKEN:
	    n = *feedback++;
	    /* TRACE(" POLYGON(%d)\n", n); */
	    while (n--)
		feedback = accum_feedback_rect(d3dp, feedback, rect, v++);
	    break;
	default:
	    FIXME("unhandled feedback token %d\n", tok);
	}
    }
    return v;
}

void GL_GetPrimitiveType(LPD3DHAL_DP2COMMAND cmd,
				GLenum* lppt, LPDWORD lpdwCount,
				LPBOOL lpbIndexed)
{
    DWORD dwCount;
    GLenum pt;
    BOOL indexed = FALSE;

    switch (cmd->bCommand)
    {
    case D3DDP2OP_POINTS:
	/* Special case. */
	TRACE("Draw POINTS\n");
	pt = GL_POINTS;
	dwCount = 0;
	indexed = FALSE;
	break;

    case D3DDP2OP_INDEXEDLINELIST2:
	indexed = TRUE;
    case D3DDP2OP_LINELIST:
	TRACE("Draw LINES\n");
	pt = GL_LINES;
	dwCount = cmd->u.wPrimitiveCount*2;
	break;

    case D3DDP2OP_INDEXEDLINESTRIP:
	indexed = TRUE;
    case D3DDP2OP_LINESTRIP:
	TRACE("Draw LINE_STRIP\n");
	pt = GL_LINE_STRIP;
	dwCount = cmd->u.wPrimitiveCount+1;
	break;

    case D3DDP2OP_INDEXEDTRIANGLELIST2:
	indexed = TRUE;
    case D3DDP2OP_TRIANGLELIST:
	TRACE("Draw TRIANGLES\n");
	pt = GL_TRIANGLES;
	dwCount = cmd->u.wPrimitiveCount*3;
	break;

    case D3DDP2OP_INDEXEDTRIANGLESTRIP:
	indexed = TRUE;
    case D3DDP2OP_TRIANGLESTRIP:
	TRACE("Draw TRIANGLE_STRIP\n");
	pt = GL_TRIANGLE_STRIP;
	dwCount = cmd->u.wPrimitiveCount+2;
	break;

    case D3DDP2OP_INDEXEDTRIANGLEFAN:
	indexed = TRUE;
    case D3DDP2OP_TRIANGLEFAN:
	TRACE("Draw TRIANGLE_FAN\n");
	pt = GL_TRIANGLE_FAN;
	dwCount = cmd->u.wPrimitiveCount+2;
	break;

    default:
	ERR("Unhandled primitive\n");
	pt = 0;
	dwCount = 0;
	indexed = FALSE;
	break;
    }

    *lpdwCount = dwCount;
    *lppt = pt;
    *lpbIndexed = indexed;
}

static inline void D3D_GL_update_invert(GL_D3D_priv* priv, BOOL invert)
{
    TRACE("invert=%d\n", invert);
    priv->glFlags |= GLF_NEW_TRANSFORM;
    priv->state.dirty_viewport = TRUE;
#ifndef HAVE_OSMESA
    glFrontFace(invert ? GL_CCW : GL_CW);
#else
    glFrontFace(invert ? GL_CW : GL_CCW);
#endif
}

HRESULT D3D_GL_draw(GL_D3D_priv* priv, LPD3DHAL_DP2COMMAND cmd, LPVOID data1, LPVOID data2)
{
    const WORD* indices;
    DWORD fvf;
    DWORD dwCount;
    DWORD dwIndexBase;
    DWORD dwVStart, dwVCount;
    DWORD flags = priv->glFlags;
    GLenum pt;
    BOOL indexed;
    BOOL calc_rect = FALSE, invert = FALSE, intercept = FALSE;
    GLfloat *feedback = NULL;
    unsigned feedback_size = 0;

    GL_GetPrimitiveType(cmd, &pt, &dwCount, &indexed);

    indices = indexed ? data2 : NULL;
    dwIndexBase = indexed ? ((LPD3DHAL_DP2STARTVERTEX)data1)->wVStart : 0;
    dwVStart = priv->dp2_dwVertexOffset;
    dwVCount = priv->dp2_dwVertexLength;

    if (priv->callbacks.D3dRenderLock)
      calc_rect = priv->callbacks.D3dRenderLock(priv->d3dp->dwhContext, TRUE, &invert);

    if (invert) priv->glFlags |= GLF_RENDER_INVERTED;
    else priv->glFlags &= ~GLF_RENDER_INVERTED;

    if (priv->glFlags & GLF_INITIALIZE) D3D_GL_initialize(priv);

    if (priv->glFlags & GLF_NEW_RENDER_STATES)
      D3D_GL_update_render_states(priv);
    if (priv->glFlags & GLF_NEW_TEXTURE_STATES)
      D3D_GL_update_texture_stage_states(priv);
    D3D_GL_update_palettes(priv);

    if ((flags ^ priv->glFlags) & GLF_RENDER_INVERTED)
      D3D_GL_update_invert(priv, invert);

    if (priv->state.dirty_viewport)
      D3D_GL_update_viewport(priv);

    /* Assume we can until proven otherwise. */
    priv->glFlags |= GLF_USE_VERTEX_ARRAYS;

    if (priv->glFlags & GLF_VERTEX_PROGRAM) {
	fvf = 0;
	/* load up vertex program stuff */
	D3D_GL_setup_programmable(priv, data1, indices, pt, dwCount, indexed);
	if (calc_rect && priv->callbacks.D3dRenderArea) {
	  priv->callbacks.D3dRenderArea(priv->d3dp->dwhContext, NULL, FALSE);
	  calc_rect = FALSE;
	}
    } else {
	fvf = D3D_GL_setup_fixed(priv, data1, indices, pt, dwCount, indexed);
	if (!fvf) {
	  if (priv->callbacks.D3dRenderUnlock)
	    priv->callbacks.D3dRenderUnlock(priv->d3dp->dwhContext, TRUE);
	  return E_FAIL;
	}
    }

    intercept = intercept_mode;

render_start:
    if (!feedback && (intercept || (calc_rect && priv->callbacks.D3dRenderArea))) {
	/* Buffer size estimation is based on:
	 * Point: token + 2 coords = 3
	 * Triangle: token + vcount + 6 coords = 8
	 * but a triangle can be clipped, after which it's usually a quad,
	 * which is represented as such, or as two triangles, depending on the implementation.
	 * Quad: token + vcount + 8 coords = 10
	 * 2 * Triangle = 16
	 *
	 * A triangle can be clipped against more than one clip plane, and in the worst case
	 * another triangle might get added for each clip per triangle, but in most cases this
	 * estimate is good enough. In the rare case it's not, we can double the buffer size and
	 * try again, but we'll add room for an extra triangle or two to avoid this in cases
	 * with few polygons rendered per call (and with many polygons, we usually happen to
	 * have enough spare room anyway).
	 */
	unsigned mp = (pt == GL_POINTS) ? 3 : 16;
	feedback_size = mp*(cmd->u.wPrimitiveCount+2);
	if (intercept) {
	    GLfloat proj_mat[16];
	    GLint view[4];
	    POINT pos;
	    GetCursorPos(&pos);
	    TRACE("entering feedback mode to intercept primitives at %ld,%ld (buffer size %d)\n", pos.x, pos.y, feedback_size);
	    glMatrixMode(GL_PROJECTION);
	    glGetFloatv(GL_PROJECTION_MATRIX, proj_mat);
	    glGetIntegerv(GL_VIEWPORT, view);
	    glPushMatrix();
	    glLoadIdentity();
	    gluPickMatrix(pos.x, view[3] - pos.y, 1, 1, view);
	    glMultMatrixf(proj_mat);
	} else {
	    TRACE("entering feedback mode to compute bounds (buffer size %d)\n", feedback_size);
	}
	feedback = HeapAlloc(GetProcessHeap(), 0, feedback_size*sizeof(GLfloat));
	glFeedbackBuffer(feedback_size, GL_2D, feedback);
	glRenderMode(GL_FEEDBACK);
    }

    TRACE("start\n");

    if (pt == GL_POINTS)
    {
	LPD3DHAL_DP2POINTS dat = (LPD3DHAL_DP2POINTS)data2;
	DWORD p;

	for (p=0; p<cmd->u.wPrimitiveCount; p++,dat++) {
	    if (priv->glFlags & GLF_USE_VERTEX_ARRAYS)
		glDrawArrays(pt, dat->wVStart, dat->wCount);
	    else
		GL_draw_vertices(priv, pt, fvf, dat->wVStart, dat->wCount,
				 GL_DirectIndex, NULL, dwIndexBase);
	}
    }
    else if (indexed)
    {
	if (priv->glFlags & GLF_USE_VERTEX_ARRAYS) {
#if 1
	    glDrawRangeElements(pt, dwVStart - dwIndexBase,
				dwVStart - dwIndexBase + dwVCount,
				dwCount, GL_UNSIGNED_SHORT, indices);
#else
	    glDrawElements(pt, dwCount, GL_UNSIGNED_SHORT, indices);
#endif
	} else
	    GL_draw_vertices(priv, pt, fvf, 0, dwCount,
			     GL_IndexedIndex, data2, dwIndexBase);
    }
    else
    {
	LPD3DHAL_DP2STARTVERTEX dat = (LPD3DHAL_DP2STARTVERTEX)data1;

	if (priv->glFlags & GLF_USE_VERTEX_ARRAYS)
	    glDrawArrays(pt, dwVStart + dat->wVStart, dwCount);
	else
	    GL_draw_vertices(priv, pt, fvf, dwVStart + dat->wVStart, dwCount,
			     GL_DirectIndex, NULL, dwIndexBase);
    }

    CHECK_GL_ERROR("DrawPrimitive");

    if (feedback) {
	int count = glRenderMode(GL_RENDER);
	RECT rect;
	TRACE("left feedback mode, %d entries\n", count);
	if (count < 0) {
	    WARN("feedback buffer estimate wrong, retrying\n");
#if 1
	    /* try again with larger feedback buffer */
	    HeapFree(GetProcessHeap(), 0, feedback);
	    feedback_size *= 2;
	    feedback = HeapAlloc(GetProcessHeap(), 0, feedback_size*sizeof(GLfloat));
	    glFeedbackBuffer(feedback_size, GL_2D, feedback);
	    glRenderMode(GL_FEEDBACK);
	    goto render_start;
#else
	    /* alternatively, we could just go with what we have,
	     * but I'm not sure if that would work. */
	    count = feedback_size;
#endif
	}
	if (intercept) {
	  if (count > 0) {
	    TRACE("PRIMITIVE INTERCEPTED!!\n");
	  }
	  glPopMatrix();
	  intercept = FALSE;
	} else {
	  if (calc_feedback_rect(priv, feedback, count, &rect))
	    priv->callbacks.D3dRenderArea(priv->d3dp->dwhContext, &rect, FALSE);
	  calc_rect = FALSE;
	}
	HeapFree(GetProcessHeap(), 0, feedback);
	feedback = NULL;
	/* now do the actual rendering */
	goto render_start;
    }

    if (priv->glFlags & GLF_VERTEX_PROGRAM) {
        /* finish up vertex program stuff */
        D3D_GL_finish_programmable(priv);
    }
    else {
	D3D_GL_finish_fixed(priv);
    }

    if (priv->callbacks.D3dRenderUnlock)
      priv->callbacks.D3dRenderUnlock(priv->d3dp->dwhContext, TRUE);

    TRACE("done\n");
    return D3D_OK;
}

HRESULT D3D_GL_clear(GL_D3D_priv* priv, LPD3DHAL_DP2COMMAND cmd, LPVOID data1, LPVOID data2)
{
    LPD3DHAL_DP2CLEAR dat = (LPD3DHAL_DP2CLEAR)data1;
    DWORD flags = priv->glFlags;
    BOOL invert = FALSE;
    GLboolean ztest = GL_FALSE;
    GLbitfield mask = 0;
    D3DRECT *rects = (D3DRECT *)data2;
    DWORD dwCount = cmd->u.wStateCount;

    TRACE("Flags=%8lx\n", dat->dwFlags);

    if (dat->dwFlags & D3DCLEAR_TARGET) {
	mask |= GL_COLOR_BUFFER_BIT;
	glClearColor(RGBA_GETRED(dat->dwFillColor) / 255.0,
		     RGBA_GETGREEN(dat->dwFillColor) / 255.0,
		     RGBA_GETBLUE(dat->dwFillColor) / 255.0,
		     RGBA_GETALPHA(dat->dwFillColor) / 255.0);
    }
    if (dat->dwFlags & D3DCLEAR_STENCIL) {
	mask |= GL_STENCIL_BUFFER_BIT;
	glClearStencil(dat->dwFillStencil);
    }
    if (dat->dwFlags & D3DCLEAR_ZBUFFER) {
	mask |= GL_DEPTH_BUFFER_BIT;
	glClearDepth(dat->dvFillDepth);
	glGetBooleanv(GL_DEPTH_WRITEMASK, &ztest);
	glDepthMask(GL_TRUE); /* Enables Z writing to be sure to delete also the Z buffer */
    }

    if (priv->callbacks.D3dRenderLock) {
      if (priv->callbacks.D3dRenderLock(priv->d3dp->dwhContext, TRUE, &invert)) {
        RECT lockrect;
        if( rects && dwCount > 0 ) {
          int i;
          /* compute the union of all the rects (in D3D coords) */
          lockrect.left   = rects[0].x1;
          lockrect.right  = rects[0].x2;
          lockrect.top    = rects[0].y1;
          lockrect.bottom = rects[0].y2;
          for( i=1; i<dwCount; i++ ) {
	    lockrect.left   = min( rects[i].x1, lockrect.left );
	    lockrect.right  = max( rects[i].x2, lockrect.right );
	    lockrect.top    = min( rects[i].y1, lockrect.top );
	    lockrect.bottom = max( rects[i].y2, lockrect.bottom );
          }
        } else {
          /* same rectangle as D3D_GL_viewport_info works with */
          lockrect.left = priv->state.viewport.dwX;
          lockrect.right = lockrect.left + priv->state.viewport.dwWidth;
          lockrect.top = priv->state.viewport.dwY;
          lockrect.bottom = lockrect.top + priv->state.viewport.dwHeight;
	}
        if ((dat->dwFlags & D3DCLEAR_TARGET) &&
            (lockrect.right > lockrect.left) && (lockrect.bottom > lockrect.top))
          priv->callbacks.D3dRenderArea(priv->d3dp->dwhContext, &lockrect, TRUE);
      }
    }

    if (invert) priv->glFlags |= GLF_RENDER_INVERTED;
    else priv->glFlags &= ~GLF_RENDER_INVERTED;

    if (priv->glFlags & GLF_INITIALIZE) D3D_GL_initialize(priv);

    if ((flags ^ priv->glFlags) & GLF_RENDER_INVERTED)
      D3D_GL_update_invert(priv, invert);

    if (priv->state.dirty_viewport)
      D3D_GL_update_viewport(priv);

    /* we probably don't need to apply render states here, or do we? */

    if( rects ) {
      GLint x,y,width,height;
      int i;
      TRACE("clearing %ld rects(%p)\n", dwCount, rects);
      /* loop through each of the rects, clearing them consecutively */
      for( i=0; i<dwCount; i++ ) {
	width = rects[i].x2 - rects[i].x1;
	height = rects[i].y2 - rects[i].y1;
	x = rects[i].x1;
#ifndef HAVE_OSMESA
	if (!(priv->glFlags & GLF_RENDER_INVERTED))
#else
	if (priv->glFlags & GLF_RENDER_INVERTED)
#endif
	  y = priv->d3dp->surf->lpGbl->wHeight - (rects[i].y1 + height);
	else
	  y = rects[i].y1;

	/* restrict the clear area to the current rectangle */
	glScissor(x,y,width,height);
	glClear(mask);
      }
      /* reset the viewport */
      priv->state.dirty_viewport = TRUE;
      D3D_GL_update_viewport(priv);
    } else {
      /* clear whatever the current viewport is */
      glClear(mask);
    }

    CHECK_GL_ERROR("Clear");

    if (priv->callbacks.D3dRenderUnlock)
      priv->callbacks.D3dRenderUnlock(priv->d3dp->dwhContext, TRUE);

    if (dat->dwFlags & D3DCLEAR_ZBUFFER)
	glDepthMask(ztest);

    return D3D_OK;
}
