#include <assert.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <stdint.h>

#include "eglconfig.h"
#include "eglcontext.h"
#include "egldisplay.h"
#include "egldriver.h"
#include "eglglobals.h"
#include "eglmode.h"
#include "eglscreen.h"
#include "eglsurface.h"
#include "egllog.h"

#include "intel_egl.h"

#include "xf86drm.h"
#include "xf86drmMode.h"

#include "intel_context.h"

#include "state_tracker/st_public.h"

#define MAX_SCREENS 16

static void
drm_get_device_id(struct egl_drm_device *device)
{
	char path[512];
	FILE *file;

	/* TODO get the real minor */
	int minor = 0;

	snprintf(path, sizeof(path), "/sys/class/drm/card%d/device/device", minor);
	file = fopen(path, "r");
	if (!file) {
		_eglLog(_EGL_WARNING, "Could not retrive device ID\n");
		return;
	}

	fgets(path, sizeof( path ), file);
	sscanf(path, "%x", &device->deviceID);
	fclose(file);
}

static struct egl_drm_device*
egl_drm_create_device(int drmFD)
{
	struct egl_drm_device *device = malloc(sizeof(*device));
	memset(device, 0, sizeof(*device));
	device->drmFD = drmFD;

	device->version = drmGetVersion(device->drmFD);

	drm_get_device_id(device);

	if (!intel_create_device(device)) {
		free(device);
		return NULL;
	}

	return device;
}

static void
_egl_context_modes_destroy(__GLcontextModes *modes)
{
   _eglLog(_EGL_DEBUG, "%s", __FUNCTION__);

   while (modes) {
      __GLcontextModes * const next = modes->next;
      free(modes);
      modes = next;
   }
}
/**
 * Create a linked list of 'count' GLcontextModes.
 * These are used during the client/server visual negotiation phase,
 * then discarded.
 */
static __GLcontextModes *
_egl_context_modes_create(unsigned count, size_t minimum_size)
{
   /* This code copied from libGLX, and modified */
   const size_t size = (minimum_size > sizeof(__GLcontextModes))
       ? minimum_size : sizeof(__GLcontextModes);
   __GLcontextModes * head = NULL;
   __GLcontextModes ** next;
   unsigned   i;

   _eglLog(_EGL_DEBUG, "%s %d %d", __FUNCTION__, count, minimum_size);

   next = & head;
   for (i = 0 ; i < count ; i++) {
      *next = (__GLcontextModes *) calloc(1, size);
      if (*next == NULL) {
	 _egl_context_modes_destroy(head);
	 head = NULL;
	 break;
      }
      
      (*next)->doubleBufferMode = 1;
      (*next)->visualID = GLX_DONT_CARE;
      (*next)->visualType = GLX_DONT_CARE;
      (*next)->visualRating = GLX_NONE;
      (*next)->transparentPixel = GLX_NONE;
      (*next)->transparentRed = GLX_DONT_CARE;
      (*next)->transparentGreen = GLX_DONT_CARE;
      (*next)->transparentBlue = GLX_DONT_CARE;
      (*next)->transparentAlpha = GLX_DONT_CARE;
      (*next)->transparentIndex = GLX_DONT_CARE;
      (*next)->xRenderable = GLX_DONT_CARE;
      (*next)->fbconfigID = GLX_DONT_CARE;
      (*next)->swapMethod = GLX_SWAP_UNDEFINED_OML;
      (*next)->bindToTextureRgb = GLX_DONT_CARE;
      (*next)->bindToTextureRgba = GLX_DONT_CARE;
      (*next)->bindToMipmapTexture = GLX_DONT_CARE;
      (*next)->bindToTextureTargets = 0;
      (*next)->yInverted = GLX_DONT_CARE;

      next = & ((*next)->next);
   }

   return head;
}

struct drm_screen;

struct drm_driver
{
	_EGLDriver base;  /* base class/object */

	drmModeResPtr res;

	struct drm_screen *screens[MAX_SCREENS];
	size_t count_screens;

	struct egl_drm_device *device;
};

struct drm_surface
{
	_EGLSurface base;  /* base class/object */

	struct egl_drm_drawable *drawable;
};

struct drm_context
{
	_EGLContext base;  /* base class/object */

	struct egl_drm_context *context;
};

struct drm_screen
{
	_EGLScreen base;

	/* currently only support one connector */
	drmModeConnectorPtr connector;

	/* Has this screen been shown */
	int shown;

	/* Surface that is currently attached to this screen */
	struct drm_surface *surf;

	/* backing buffer */
	drmBO buffer;

	/* framebuffer */
	drmModeFBPtr fb;
	uint32_t fbID;

	/* crtc and mode used */
	drmModeCrtcPtr crtc;
	uint32_t crtcID;

	struct drm_mode_modeinfo *mode;

	/* geometry of the screen */
	struct egl_drm_frontbuffer front;
};

static void
drm_update_res(struct drm_driver *drm_drv)
{
	drmModeFreeResources(drm_drv->res);
	drm_drv->res = drmModeGetResources(drm_drv->device->drmFD);
}

static void
drm_add_modes_from_connector(_EGLScreen *screen, drmModeConnectorPtr connector)
{
	struct drm_mode_modeinfo *m;
	int i;

	for (i = 0; i < connector->count_modes; i++) {
		m = &connector->modes[i];
		_eglAddNewMode(screen, m->hdisplay, m->vdisplay, m->vrefresh, m->name);
	}
}


static EGLBoolean
drm_initialize(_EGLDriver *drv, EGLDisplay dpy, EGLint *major, EGLint *minor)
{
	_EGLDisplay *disp = _eglLookupDisplay(dpy);
	struct drm_driver *drm_drv = (struct drm_driver *)drv;
	struct drm_screen *screen = NULL;
	drmModeConnectorPtr connector = NULL;
	drmModeResPtr res = NULL;
	unsigned count_connectors = 0;
	int num_screens = 0;

	EGLint i;
	int fd;

	fd = drmOpen("i915", NULL);
	if (fd < 0) {
		return EGL_FALSE;
	}

	drm_drv->device = egl_drm_create_device(fd);
	if (!drm_drv->device) {
		drmClose(fd);
		return EGL_FALSE;
	}

	drm_update_res(drm_drv);
	res = drm_drv->res;
	if (res)
		count_connectors = res->count_connectors;

	for(i = 0; i < count_connectors && i < MAX_SCREENS; i++) {
		connector = drmModeGetConnector(fd, res->connectors[i]);

		if (!connector)
			continue;

		if (connector->connection != DRM_MODE_CONNECTED) {
			drmModeFreeConnector(connector);
			continue;
		}

		screen = malloc(sizeof(struct drm_screen));
		memset(screen, 0, sizeof(*screen));
		screen->connector = connector;
		_eglInitScreen(&screen->base);
		_eglAddScreen(disp, &screen->base);
		drm_add_modes_from_connector(&screen->base, connector);
		drm_drv->screens[num_screens++] = screen;
	}
	drm_drv->count_screens = num_screens;

	/* for now we only have one config */
	_EGLConfig *config = calloc(1, sizeof(*config));
	memset(config, 1, sizeof(*config));
	_eglInitConfig(config, 1);
	_eglSetConfigAttrib(config, EGL_RED_SIZE, 8);
	_eglSetConfigAttrib(config, EGL_GREEN_SIZE, 8);
	_eglSetConfigAttrib(config, EGL_BLUE_SIZE, 8);
	_eglSetConfigAttrib(config, EGL_ALPHA_SIZE, 8);
	_eglSetConfigAttrib(config, EGL_BUFFER_SIZE, 32);
	_eglSetConfigAttrib(config, EGL_DEPTH_SIZE, 24);
	_eglSetConfigAttrib(config, EGL_STENCIL_SIZE, 8);
	_eglSetConfigAttrib(config, EGL_SURFACE_TYPE, EGL_PBUFFER_BIT);
	_eglAddConfig(disp, config);

	drv->Initialized = EGL_TRUE;

	*major = 1;
	*minor = 4;

	return EGL_TRUE;
}

static void
drm_takedown_shown_screen(_EGLDriver *drv, struct drm_screen *screen)
{
	struct drm_driver *drm_drv = (struct drm_driver *)drv;
	unsigned int i;

	intel_bind_frontbuffer(screen->surf->drawable, NULL);
	screen->surf = NULL;

	for (i = 0; i < drm_drv->res->count_crtcs; i++) {
		drmModeSetCrtc(
			drm_drv->device->drmFD,
			drm_drv->res->crtcs[i],
			0, // FD
			0, 0,
			NULL, 0, // List of output ids
			NULL);
	}

	drmModeRmFB(drm_drv->device->drmFD, screen->fbID);
	drmModeFreeFB(screen->fb);
	screen->fb = NULL;

	drmBOUnreference(drm_drv->device->drmFD, &screen->buffer);

	screen->shown = 0;
}

static EGLBoolean
drm_terminate(_EGLDriver *drv, EGLDisplay dpy)
{
	struct drm_driver *drm_drv = (struct drm_driver *)drv;
	struct drm_screen *screen;
	int i = 0;

	intel_destroy_device(drm_drv->device);
	drmFreeVersion(drm_drv->device->version);

	for (i = 0; i < drm_drv->count_screens; i++) {
		screen = drm_drv->screens[i];

		if (screen->shown)
			drm_takedown_shown_screen(drv, screen);

		drmModeFreeConnector(screen->connector);
		_eglDestroyScreen(&screen->base);
		drm_drv->screens[i] = NULL;
	}

	drmClose(drm_drv->device->drmFD);

	free(drm_drv->device);

	_eglCleanupDisplay(_eglLookupDisplay(dpy));
	free(drm_drv);

	return EGL_TRUE;
}


static struct drm_context *
lookup_drm_context(EGLContext context)
{
	_EGLContext *c = _eglLookupContext(context);
	return (struct drm_context *) c;
}


static struct drm_surface *
lookup_drm_surface(EGLSurface surface)
{
	_EGLSurface *s = _eglLookupSurface(surface);
	return (struct drm_surface *) s;
}

static struct drm_screen *
lookup_drm_screen(EGLDisplay dpy, EGLScreenMESA screen)
{
	_EGLScreen *s = _eglLookupScreen(dpy, screen);
	return (struct drm_screen *) s;
}

static __GLcontextModes*
visual_from_config(_EGLConfig *conf)
{
	__GLcontextModes *visual;
	(void)conf;

	visual = _egl_context_modes_create(1, sizeof(*visual));
	visual->redBits = 8;
	visual->greenBits = 8;
	visual->blueBits = 8;
	visual->alphaBits = 8;

	visual->rgbBits = 32;
	visual->doubleBufferMode = 1;

	visual->depthBits = 24;
	visual->haveDepthBuffer = visual->depthBits > 0;
	visual->stencilBits = 8;
	visual->haveStencilBuffer = visual->stencilBits > 0;

	return visual;
}



static EGLContext
drm_create_context(_EGLDriver *drv, EGLDisplay dpy, EGLConfig config, EGLContext share_list, const EGLint *attrib_list)
{
	struct drm_driver *drm_drv = (struct drm_driver *)drv;
	struct drm_context *c;
	struct drm_egl_context *share = NULL;
	_EGLConfig *conf;
	int i;
	int ret;
	__GLcontextModes *visual;
	struct egl_drm_context *context;

	conf = _eglLookupConfig(drv, dpy, config);
	if (!conf) {
		_eglError(EGL_BAD_CONFIG, "eglCreateContext");
		return EGL_NO_CONTEXT;
	}

	for (i = 0; attrib_list && attrib_list[i] != EGL_NONE; i++) {
		switch (attrib_list[i]) {
			/* no attribs defined for now */
			default:
				_eglError(EGL_BAD_ATTRIBUTE, "eglCreateContext");
				return EGL_NO_CONTEXT;
		}
	}

	c = (struct drm_context *) calloc(1, sizeof(struct drm_context));
	if (!c)
		return EGL_NO_CONTEXT;

	_eglInitContext(drv, dpy, &c->base, config, attrib_list);

	context = malloc(sizeof(*context));
	memset(context, 0, sizeof(*context));

	if (!context)
		goto err_c;

	context->device = drm_drv->device;
	visual = visual_from_config(conf);

	ret = intel_create_context(context, visual, share);
	free(visual);

	if (!ret)
		goto err_gl;

	c->context = context;

	/* generate handle and insert into hash table */
	_eglSaveContext(&c->base);
	assert(_eglGetContextHandle(&c->base));

	return _eglGetContextHandle(&c->base);
err_gl:
	free(context);
err_c:
	free(c);
	return EGL_NO_CONTEXT;
}

static EGLBoolean
drm_destroy_context(_EGLDriver *drv, EGLDisplay dpy, EGLContext context)
{
	struct drm_context *fc = lookup_drm_context(context);
	_eglRemoveContext(&fc->base);
	if (fc->base.IsBound) {
		fc->base.DeletePending = EGL_TRUE;
	} else {
		intel_destroy_context(fc->context);
		free(fc->context);
		free(fc);
	}
	return EGL_TRUE;
}


static EGLSurface
drm_create_window_surface(_EGLDriver *drv, EGLDisplay dpy, EGLConfig config, NativeWindowType window, const EGLint *attrib_list)
{
	return EGL_NO_SURFACE;
}


static EGLSurface
drm_create_pixmap_surface(_EGLDriver *drv, EGLDisplay dpy, EGLConfig config, NativePixmapType pixmap, const EGLint *attrib_list)
{
	return EGL_NO_SURFACE;
}


static EGLSurface
drm_create_pbuffer_surface(_EGLDriver *drv, EGLDisplay dpy, EGLConfig config,
                         const EGLint *attrib_list)
{
	struct drm_driver *drm_drv = (struct drm_driver *)drv;
	int i;
	int ret;
	int width = -1;
	int height = -1;
	struct drm_surface *surf = NULL;
	struct egl_drm_drawable *drawable = NULL;
	__GLcontextModes *visual;
	_EGLConfig *conf;

	conf = _eglLookupConfig(drv, dpy, config);
	if (!conf) {
		_eglError(EGL_BAD_CONFIG, "eglCreatePbufferSurface");
		return EGL_NO_CONTEXT;
	}

	for (i = 0; attrib_list && attrib_list[i] != EGL_NONE; i++) {
		switch (attrib_list[i]) {
			case EGL_WIDTH:
				width = attrib_list[++i];
				break;
			case EGL_HEIGHT:
				height = attrib_list[++i];
				break;
			default:
				_eglError(EGL_BAD_ATTRIBUTE, "eglCreatePbufferSurface");
				return EGL_NO_SURFACE;
		}
	}

	if (width < 1 || height < 1) {
		_eglError(EGL_BAD_ATTRIBUTE, "eglCreatePbufferSurface");
		return EGL_NO_SURFACE;
	}

	surf = (struct drm_surface *) calloc(1, sizeof(struct drm_surface));
	if (!surf)
		goto err;

	if (!_eglInitSurface(drv, dpy, &surf->base, EGL_PBUFFER_BIT, config, attrib_list))
		goto err_surf;

	drawable = malloc(sizeof(*drawable));
	memset(drawable, 0, sizeof(*drawable));

	drawable->w = width;
	drawable->h = height;

	visual = visual_from_config(conf);

	drawable->device = drm_drv->device;
	ret = intel_create_drawable(drawable, visual);
	free(visual);

	if (!ret)
		goto err_draw;

	surf->drawable = drawable;

	_eglSaveSurface(&surf->base);
	return surf->base.Handle;

err_draw:
	free(drawable);
err_surf:
	free(surf);
err:
	return EGL_NO_SURFACE;
}

static EGLSurface
drm_create_screen_surface_mesa(_EGLDriver *drv, EGLDisplay dpy, EGLConfig cfg,
                               const EGLint *attrib_list)
{
	EGLSurface surf = drm_create_pbuffer_surface(drv, dpy, cfg, attrib_list);

	return surf;
}

static struct drm_mode_modeinfo *
drm_find_mode(drmModeConnectorPtr connector, _EGLMode *mode)
{
	int i;
	struct drm_mode_modeinfo *m;

	for (i = 0; i < connector->count_modes; i++) {
		m = &connector->modes[i];
		if (m->hdisplay == mode->Width && m->vdisplay == mode->Height && m->vrefresh == mode->RefreshRate)
			break;
		m = &connector->modes[0]; /* if we can't find one, return first */
	}

	return m;
}
static void
draw(size_t x, size_t y, size_t w, size_t h, size_t pitch, size_t v, unsigned int *ptr)
{
    int i, j;

    for (i = x; i < x + w; i++)
        for(j = y; j < y + h; j++)
            ptr[(i * pitch / 4) + j] = v;

}

static void
prettyColors(int fd, unsigned int handle, size_t pitch)
{
	drmBO bo;
	unsigned int *ptr;
	void *p;
	int i;

	drmBOReference(fd, handle, &bo);
	drmBOMap(fd, &bo, DRM_BO_FLAG_READ | DRM_BO_FLAG_WRITE, 0, &p);
	ptr = (unsigned int*)p;

	for (i = 0; i < (bo.size / 4); i++)
		ptr[i] = 0xFFFFFFFF;

	for (i = 0; i < 4; i++)
		draw(i * 40, i * 40, 40, 40, pitch, 0, ptr);


	draw(200, 100, 40, 40, pitch, 0xff00ff, ptr);
	draw(100, 200, 40, 40, pitch, 0xff00ff, ptr);

	drmBOUnmap(fd, &bo);
}

static EGLBoolean
drm_show_screen_surface_mesa(_EGLDriver *drv, EGLDisplay dpy,
                         EGLScreenMESA screen,
                         EGLSurface surface, EGLModeMESA m)
{
	struct drm_driver *drm_drv = (struct drm_driver *)drv;
	struct drm_surface *surf = lookup_drm_surface(surface);
	struct drm_screen *scrn = lookup_drm_screen(dpy, screen);
	_EGLMode *mode = _eglLookupMode(dpy, m);
	size_t pitch = mode->Width * 4;
	size_t size = mode->Height * pitch;
	int ret;
	unsigned int i,j,k;

	if (scrn->shown)
		drm_takedown_shown_screen(drv, scrn);

	ret = drmBOCreate(drm_drv->device->drmFD, size, 0, 0,
		DRM_BO_FLAG_READ |
		DRM_BO_FLAG_WRITE |
		DRM_BO_FLAG_MEM_TT |
		DRM_BO_FLAG_MEM_VRAM |
		DRM_BO_FLAG_NO_EVICT,
		DRM_BO_HINT_DONT_FENCE, &scrn->buffer);

	if (ret)
		return EGL_FALSE;

	prettyColors(drm_drv->device->drmFD, scrn->buffer.handle, pitch);

	ret = drmModeAddFB(drm_drv->device->drmFD, mode->Width, mode->Height,
			32, 32, pitch,
			scrn->buffer.handle,
			&scrn->fbID);

	if (ret)
		goto err_bo;

	scrn->fb = drmModeGetFB(drm_drv->device->drmFD, scrn->fbID);
	if (!scrn->fb)
		goto err_bo;

	for (j = 0; j < drm_drv->res->count_connectors; j++) {
		drmModeConnector *con = drmModeGetConnector(drm_drv->device->drmFD, drm_drv->res->connectors[j]);
		scrn->mode = drm_find_mode(con, mode);
		if (!scrn->mode)
			goto err_fb;

		for (k = 0; k < con->count_encoders; k++) {
			drmModeEncoder *enc = drmModeGetEncoder(drm_drv->device->drmFD, con->encoders[k]);
			for (i = 0; i < drm_drv->res->count_crtcs; i++) {
				if (enc->possible_crtcs & (1<<i)) {
					ret = drmModeSetCrtc(
						drm_drv->device->drmFD,
						drm_drv->res->crtcs[i],
						scrn->fbID,
						0, 0,
						&drm_drv->res->connectors[j], 1,
						scrn->mode);	
					/* skip the other crtcs now */
					i = drm_drv->res->count_crtcs;
				}
			}
		}
	}

	scrn->front.handle = scrn->buffer.handle;
	scrn->front.pitch = pitch;
	scrn->front.width = mode->Width;
	scrn->front.height = mode->Height;

	scrn->surf = surf;
	intel_bind_frontbuffer(surf->drawable, &scrn->front);

	scrn->shown = 1;

	return EGL_TRUE;

err_fb:
	/* TODO remove fb */

err_bo:
	drmBOUnreference(drm_drv->device->drmFD, &scrn->buffer);
	return EGL_FALSE;
}

static EGLBoolean
drm_destroy_surface(_EGLDriver *drv, EGLDisplay dpy, EGLSurface surface)
{
	struct drm_surface *fs = lookup_drm_surface(surface);
	_eglRemoveSurface(&fs->base);
	if (fs->base.IsBound) {
		fs->base.DeletePending = EGL_TRUE;
	} else {
		intel_bind_frontbuffer(fs->drawable, NULL);
		intel_destroy_drawable(fs->drawable);
		free(fs->drawable);
		free(fs);
	}
	return EGL_TRUE;
}


static EGLBoolean
drm_make_current(_EGLDriver *drv, EGLDisplay dpy, EGLSurface draw, EGLSurface read, EGLContext context)
{
	struct drm_surface *readSurf = lookup_drm_surface(read);
	struct drm_surface *drawSurf = lookup_drm_surface(draw);
	struct drm_context *ctx = lookup_drm_context(context);
	EGLBoolean b;

	b = _eglMakeCurrent(drv, dpy, draw, read, context);
	if (!b)
		return EGL_FALSE;

	if (ctx) {
		if (!drawSurf || !readSurf)
			return EGL_FALSE;

		intel_make_current(ctx->context, drawSurf->drawable, readSurf->drawable);
	} else {
		intel_make_current(NULL, NULL, NULL);
	}

	return EGL_TRUE;
}

static EGLBoolean
drm_swap_buffers(_EGLDriver *drv, EGLDisplay dpy, EGLSurface draw)
{
	struct drm_surface *surf = lookup_drm_surface(draw);
	if (!surf)
		return EGL_FALSE;

	/* error checking */
	if (!_eglSwapBuffers(drv, dpy, draw))
		return EGL_FALSE;

	intel_swap_buffers(surf->drawable);
	return EGL_TRUE;
}


/**
 * The bootstrap function.  Return a new drm_driver object and
 * plug in API functions.
 */
_EGLDriver *
_eglMain(_EGLDisplay *dpy, const char *args)
{
	struct drm_driver *drm;

	drm = (struct drm_driver *) calloc(1, sizeof(struct drm_driver));
	if (!drm) {
		return NULL;
	}

	/* First fill in the dispatch table with defaults */
	_eglInitDriverFallbacks(&drm->base);
	/* then plug in our Drm-specific functions */
	drm->base.API.Initialize = drm_initialize;
	drm->base.API.Terminate = drm_terminate;
	drm->base.API.CreateContext = drm_create_context;
	drm->base.API.MakeCurrent = drm_make_current;
	drm->base.API.CreateWindowSurface = drm_create_window_surface;
	drm->base.API.CreatePixmapSurface = drm_create_pixmap_surface;
	drm->base.API.CreatePbufferSurface = drm_create_pbuffer_surface;
	drm->base.API.DestroySurface = drm_destroy_surface;
	drm->base.API.DestroyContext = drm_destroy_context;
	drm->base.API.CreateScreenSurfaceMESA = drm_create_screen_surface_mesa;
	drm->base.API.ShowScreenSurfaceMESA = drm_show_screen_surface_mesa;
	drm->base.API.SwapBuffers = drm_swap_buffers;

	drm->base.ClientAPIsMask = EGL_OPENGL_BIT /*| EGL_OPENGL_ES_BIT*/;
	drm->base.Name = "DRM/Gallium";

	/* enable supported extensions */
	drm->base.Extensions.MESA_screen_surface = EGL_TRUE;
	drm->base.Extensions.MESA_copy_context = EGL_TRUE;

	return &drm->base;
}