/*
 *  Surface -- Rubygame-bound SDL Surface class
 *
 * --
 *  Rubygame -- Ruby code and bindings to SDL to facilitate game creation
 *  Copyright (C) 2004-2005  John 'jacius' Croisant
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; either
 *  version 2.1 of the License, or (at your option) any later version.
 *
 *  This library 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
 *  Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 * ++
 */

#include "rubygame.h"
#include "rubygame_surface.h"

void Rubygame_Init_Surface();

VALUE cSurface;

VALUE rbgm_surface_new(int, VALUE*, VALUE);

VALUE rbgm_surface_get_w(VALUE);
VALUE rbgm_surface_get_h(VALUE);
VALUE rbgm_surface_get_size(VALUE);

VALUE rbgm_surface_get_depth(VALUE);
VALUE rbgm_surface_get_flags(VALUE);
VALUE rbgm_surface_get_masks(VALUE);

VALUE rbgm_surface_get_alpha(VALUE);
VALUE rbgm_surface_set_alpha(int, VALUE*, VALUE);

VALUE rbgm_surface_get_colorkey(VALUE);
VALUE rbgm_surface_set_colorkey(int, VALUE*, VALUE);

VALUE rbgm_surface_blit(int, VALUE*, VALUE);

VALUE rbgm_surface_fill(int, VALUE*, VALUE);

VALUE rbgm_surface_getat(int, VALUE*, VALUE);

VALUE rbgm_surface_pixels(VALUE);


/* 
 *  call-seq:
 *     new(size, flags=0)  ->  Surface
 *
 *  Create and initialize a new Surface object. 
 *
 *  A Surface is a grid of image data which you blit (i.e. copy) onto other
 *  Surfaces. Since the Rubygame display is also a Surface (see the Screen 
 *  class), Surfaces can be blit to the screen; this is the most common way
 *  to display images on the screen.
 *
 *  The new surface retrieves the pixel format of the display window to use
 *  as its own. Because of this, the display window must be created before
 *  this method can be used.
 *
 *  This function takes these arguments:
 *  size::  requested surface size; an array of the form [width, height].
 *  flags:: a bitwise OR'd ( | ) list of zero or more of the following flags
 *          (located in the Rubygame module, e.g. Rubygame::SWSURFACE).
 *          This argument may be omitted, in which case the Surface 
 *          will be a normal software surface (this is not necessarily a bad
 *          thing).
 *          SWSURFACE::   (default) request a software surface.
 *          HWSURFACE::   request a hardware-accelerated surface (using a 
 *                        graphics card), if available. Creates a software
 *                        surface if hardware surfaces are not available.
 *          SRCCOLORKEY:: request a colorkeyed surface. #set_colorkey
 *                        will enable colorkey as needed. For a description
 *                        of colorkeys, see #set_colorkey.
 *          SRCALPHA::    request an alpha channel. #set_alpha will
 *                        also enable alpha. as needed. For a description
 *                        of alpha, see #alpha.
 */
VALUE rbgm_surface_new(int argc, VALUE *argv, VALUE class)
{
	VALUE self;
	SDL_Surface *self_surf, *screen;
	SDL_PixelFormat *format;
	Uint32 flags, Rmask, Gmask, Bmask, Amask;
	int w, h, depth;
	
  flags = Rmask = Gmask = Bmask = Amask = w = h = depth = 0;

  /* Pixel format is retrieved from the video surface. */
  screen = SDL_GetVideoSurface();

  if( screen == NULL )
    {
      rb_raise(eSDLError,\
               "Could not create new Surface: a display window must be created before using this method, but none was found: %s",\
               SDL_GetError());
    }

  format = screen->format;
      
  depth = format->BitsPerPixel;
  Rmask = format->Rmask;
  Gmask = format->Gmask;
  Bmask = format->Bmask;
  Amask = format->Amask;
    

	/* Get width and height for new surface from argv[0] */
	Check_Type(argv[0],T_ARRAY);

	if(RARRAY(argv[0])->len >= 2)
	{
		w = NUM2INT(rb_ary_entry(argv[0],0));
		h = NUM2INT(rb_ary_entry(argv[0],1));
	}
	else
		rb_raise(rb_eArgError,"wrong dimensions for Surface size (%d for 2)",\
             RARRAY(argv[0])->len);
	


	/* Get flags from user, or default to 0.
   * Eventually we should accept an Array of flags.
   * Bitwise-OR is /so/ 1979. */
	if(argc > 1 && argv[1] != Qnil)
		flags = NUM2UINT(argv[1]);
	else
		flags = 0;


  /* Finally, we can create the new Surface! Or try, anyway... */
	self_surf = SDL_CreateRGBSurface(flags,w,h,depth,Rmask,Gmask,Bmask,Amask);

	if( self_surf == NULL )
		rb_raise(eSDLError,"Could not create new surface: %s",SDL_GetError());


	/* Wrap the new surface in a crunchy candy VALUE shell. */
	self = Data_Wrap_Struct( cSurface,0,SDL_FreeSurface,self_surf );
	/* The default initialize() does nothing, but may be overridden. */
	rb_obj_call_init(self,argc,argv);
	return self;
}


VALUE rbgm_surface_initialize(int argc, VALUE *argv, VALUE self)
{
	return self;
}


/* 
 *  call-seq:
 *     width
 *     w
 *
 *  Return the width (in pixels) of the surface. 
 */
VALUE rbgm_surface_get_w(VALUE self)
{
	SDL_Surface *surf;
	Data_Get_Struct(self, SDL_Surface, surf);
	return INT2NUM(surf->w);
}

/* 
 *  call-seq:
 *     height
 *     h
 *
 *  Return the height (in pixels) of the surface. 
 */
VALUE rbgm_surface_get_h(VALUE self)
{
	SDL_Surface *surf;
	Data_Get_Struct(self, SDL_Surface, surf);
	return INT2NUM(surf->h);
}

/*  call-seq:
 *    size  ->  [w,h]
 *
 *  Return the surface's width and height (in pixels) in an Array.
 */
VALUE rbgm_surface_get_size(VALUE self)
{
	SDL_Surface *surf;
	Data_Get_Struct(self, SDL_Surface, surf);
	return rb_ary_new3( 2, INT2NUM(surf->w), INT2NUM(surf->h) );
}

/*  call-seq:
 *     depth
 *
 * Return the color depth (in bits per pixel) of the surface.
 */
VALUE rbgm_surface_get_depth(VALUE self)
{
	SDL_Surface *surf;
	Data_Get_Struct(self, SDL_Surface, surf);
	return INT2NUM(surf->format->BitsPerPixel);
}

/*  call-seq:
 *     flags
 *
 *  Return any flags the surface was initialized with.
 */
VALUE rbgm_surface_get_flags(VALUE self)
{
	SDL_Surface *surf;
	Data_Get_Struct(self, SDL_Surface, surf);
	return INT2NUM(surf->flags);
}

/* 
 *  call-seq:
 *    masks  ->  [r,g,b,a]
 *
 *  Return the color masks [r,g,b,a] of the surface. Almost everyone can
 *  ignore this function. Color masks are used to separate an
 *  integer representation of a color into its seperate channels.
 */
VALUE rbgm_surface_get_masks(VALUE self)
{
	SDL_Surface *surf;
	SDL_PixelFormat *format;

	Data_Get_Struct(self, SDL_Surface, surf);
	format = surf->format;
	return rb_ary_new3(4,\
		INT2NUM(format->Rmask),\
		INT2NUM(format->Gmask),\
		INT2NUM(format->Bmask),\
		INT2NUM(format->Amask));
}

/* 
 *  call-seq:
 *     alpha
 *
 *  Return the per-surface alpha (opacity; non-transparency) of the surface.
 *  It can range from 0 (full transparent) to 255 (full opaque).
 */
VALUE rbgm_surface_get_alpha(VALUE self)
{
	SDL_Surface *surf;
	Data_Get_Struct(self, SDL_Surface, surf);
	return INT2NUM(surf->format->alpha);
}

/*
 *  call-seq:
 *     set_alpha(alpha, flags=Rubygame::SRC_ALPHA)
 *
 *  Set the per-surface alpha (opacity; non-transparency) of the surface.
 *
 *  This function takes these arguments:
 *  alpha:: requested opacity of the surface. Alpha must be from 0 
 *          (fully transparent) to 255 (fully opaque).
 *  flags:: 0 or Rubygame::SRC_ALPHA (default). Most people will want the
 *          default, in which case this argument can be omitted. For advanced
 *          users: this flag affects the surface as described in the docs for
 *          the SDL C function, SDL_SetAlpha.
 */
VALUE rbgm_surface_set_alpha(int argc, VALUE *argv, VALUE self)
{
	SDL_Surface *surf;
	Uint8 alpha;
	Uint32 flags = SDL_SRCALPHA;

	switch(argc)
	{
		case 2: flags = NUM2INT(argv[1]);
			/* no break */
		case 1: 
      {
			int temp;
			temp = NUM2INT(argv[0]);
			if(temp<0) alpha = 0;
			else if(temp>255) alpha = 255;
			else alpha = (Uint8) temp;
			break;
      }
		default:
			rb_raise(rb_eArgError,\
				"Wrong number of args to set mode (%d for 1)",argc);
	}

	Data_Get_Struct(self,SDL_Surface,surf);
	if(SDL_SetAlpha(surf,flags,alpha)!=0)
		rb_raise(eSDLError,"%s",SDL_GetError());
	return self;
}

/*
 *  call-seq:
 *     Rubygame::Surface#colorkey  ->  [r,g,b]  or  nil
 *
 *  Return the colorkey of the surface in the form [r,g,b] (or +nil+ if there
 *  is no key). The colorkey of a surface is the exact color which will be
 *  ignored when the surface is blitted, effectively turning that color
 *  transparent. This is often used to make a blue (for example) background
 *  on an image seem transparent.
 */
VALUE rbgm_surface_get_colorkey( VALUE self )
{
	SDL_Surface *surf;
	Uint32 colorkey;
	Uint8 r,g,b;

	Data_Get_Struct(self, SDL_Surface, surf);
	colorkey = surf->format->colorkey;
	if((int *)colorkey == NULL)
		return Qnil;
	SDL_GetRGB(colorkey, surf->format, &r, &g, &b);
	return rb_ary_new3(3,INT2NUM(r),INT2NUM(g),INT2NUM(b));
}

/*
 *  call-seq:
 *     set_colorkey(color,flags=0)
 *
 *  Set the colorkey of the surface. See Surface#colorkey for a description
 *  of colorkeys.
 *
 *  This method takes these arguments:
 *  color:: color to use as the key, in the form [r,g,b]. Can be +nil+ to
 *          un-set the colorkey.
 *  flags:: 0 or Rubygame::SRC_COLORKEY (default) or 
 *          Rubygame::SRC_COLORKEY|Rubygame::SDL_RLEACCEL. Most people will 
 *          want the default, in which case this argument can be omitted. For
 *          advanced users: this flag affects the surface as described in the
 *          docs for the SDL C function, SDL_SetColorkey.
 */
VALUE rbgm_surface_set_colorkey( int argc, VALUE *argv, VALUE self)
{
	SDL_Surface *surf;
	Uint32 color;
	Uint32 flag;
	Uint8 r,g,b;

	Data_Get_Struct(self, SDL_Surface, surf);
	if(argv[0] == Qnil)
	{
		flag = 0;
		color = 0;
	}
	else
	{
		if(argc > 1)
			flag = NUM2INT(argv[1]);
		else
			flag = SDL_SRCCOLORKEY;

		r = NUM2INT(rb_ary_entry(argv[0],0));
		g = NUM2INT(rb_ary_entry(argv[0],1));
		b = NUM2INT(rb_ary_entry(argv[0],2));
		//printf("RGB: %d,%d,%d  ",r,g,b);
		color = SDL_MapRGB(surf->format, r,g,b);
		//printf("colorkey: %d\n", color);
	}

	if(SDL_SetColorKey(surf,flag,color)!=0)
		rb_raise(eSDLError,"could not set colorkey: %s",SDL_GetError());
	return self;
}

/* Apparently it is not desirable to define these functions when
 * using Micrsoft Visual C.
 */
#ifndef _MSC_VER

static inline int max(int a, int b) {
  return a > b ? a : b;
}
static inline int min(int a, int b) {
  return a > b ? b : a;
}

#endif

/* 
 *  call-seq:
 *     blit(target,dest,source=nil)  ->  Rect
 *
 *  Blit (copy) all or part of the surface's image to another surface,
 *  at a given position. Returns a Rect representing the area of 
 *  +target+ which was affected by the blit.
 *
 *  This method takes these arguments:
 *  target:: the target Surface on which to paste the image.
 *  dest::   the coordinates of the top-left corner of the blit. Affects the
 *           area of +other+ the image data is /pasted/ over.
 *           Can also be a Rect or an Array larger than 2, but
 *           width and height will be ignored. 
 *  source:: a Rect representing the area of the source surface to get data
 *           from. Affects where the image data is /copied/ from.
 *           Can also be an Array of no less than 4 values. 
 */
VALUE rbgm_surface_blit(int argc, VALUE *argv, VALUE self)
{
	int left, top, right, bottom;
	int blit_x,blit_y,blit_w,blit_h;
	//int dest_x,dest_y,dest_w,dest_h;
	int src_x,src_y,src_w,src_h;
	VALUE returnrect;
	SDL_Surface *src, *dest;
	SDL_Rect *src_rect, *blit_rect;

	if(argc < 2 || argc > 3)
		rb_raise( rb_eArgError,"Wrong number of arguments to blit (%d for 2)",argc);


	Data_Get_Struct(self, SDL_Surface, src);
	Data_Get_Struct(argv[0], SDL_Surface, dest);

	blit_x = NUM2INT(rb_ary_entry(argv[1],0));
	blit_y = NUM2INT(rb_ary_entry(argv[1],1));

	/* did we get a src_rect argument or not? */
	if(argc>2 && argv[2]!=Qnil)
	{
		/* it might be good to check that it's actually a rect */
		src_x = NUM2INT(rb_ary_entry(argv[2],0));
		src_y = NUM2INT(rb_ary_entry(argv[2],1));
		src_w = NUM2INT(rb_ary_entry(argv[2],2));
		src_h = NUM2INT(rb_ary_entry(argv[2],3));
	}
	else
	{
		src_x = 0;
		src_y = 0;
		src_w = src->w;
		src_h = src->h;
	}
	src_rect = make_rect( src_x, src_y, src_w, src_h );

	/* experimental (broken) rectangle cropping code */
	/* crop if it went off left/top/right/bottom */
	//left = max(blit_x,0);
	//top = max(blit_y,0);
	//right = min(blit_x+src_w,dest->w);
	//bottom = min(blit_y+src_h,dest->h);

	left = blit_x;
	top = blit_y;
	right = blit_x+src_w;
	bottom = blit_y+src_h;
		
	//blit_w = min(blit_x+blit_w,dest->w) - max(blit_x,0);
	//blit_h = min(blit_y+blit_h,dest->h) - max(blit_y,0);
	blit_w = right - left;
	blit_h = bottom - top;
	
	blit_rect = make_rect( left, top, blit_w, blit_h );

	SDL_BlitSurface(src,src_rect,dest,blit_rect);

	returnrect = rb_funcall(cRect,rb_intern("new"),4,
		INT2NUM(left),INT2NUM(top),\
		INT2NUM(blit_w),INT2NUM(blit_h));

	free(blit_rect);
	free(src_rect);
	return returnrect;
}

/* 
 *  call-seq:
 *     fill(color,rect=nil)
 *
 *  Fill all or part of a Surface with a color.
 *
 *  This method takes these arguments:
 *  color:: color to fill with, in the form +[r,g,b]+ or +[r,g,b,a]+ (for
 *          partially transparent fills).
 *  rect::  a Rubygame::Rect representing the area of the surface to fill
 *          with color. Omit to fill the entire surface.
 */
VALUE rbgm_surface_fill( int argc, VALUE *argv, VALUE self )
{
	SDL_Surface *surf;
	SDL_Rect *rect;
	Uint32 color;
	Uint8 r,g,b,a;

	Data_Get_Struct(self, SDL_Surface, surf);

	if(argc < 1)
	{
		rb_raise(rb_eArgError,"wrong number of arguments (%d for 1 or 2)",argc);
	}

	r = FIX2UINT(rb_ary_entry(argv[0],0));
	g = FIX2UINT(rb_ary_entry(argv[0],1));
	b = FIX2UINT(rb_ary_entry(argv[0],2));
	/* if the array is larger than [R,G,B], it should be [R,G,B,A] */
	if(RARRAY(argv[0])->len > 3)
	{
		a = FIX2UINT(rb_ary_entry(argv[0],3));
		color = SDL_MapRGBA(surf->format, r,g,b,a);
	}
	else
	{
		color = SDL_MapRGB(surf->format, r,g,b);
	}

	switch(argc)
	{
		case 1: /* fill whole thing */
			SDL_FillRect(surf,NULL,color);
			break;
		case 2: /* fill a given rect */
			rect = make_rect(\
				NUM2INT(rb_ary_entry(argv[1],0)),\
				NUM2INT(rb_ary_entry(argv[1],1)),\
				NUM2INT(rb_ary_entry(argv[1],2)),\
				NUM2INT(rb_ary_entry(argv[1],3))\
			);
			SDL_FillRect(surf,rect,color);
			free(rect);
			break;
		default:
			rb_raise( rb_eArgError,"Wrong number of arguments to fill (%d for 1 or 2)",NUM2INT(argc));
			break;
	}
	return self;
}

/* 
 *  call-seq: 
 *     get_at(pos)
 *     get_at(x,y)
 *
 *  Return the color [r,g,b,a] of the pixel at the given coordinate. 
 *
 *  This method takes these argument:
 *  - pos:: the coordinate of the pixel to get the color of.
 *
 *  The coordinate can also be given as two arguments, separate +x+ and +y+
 *  positions.
 */
VALUE rbgm_surface_getat( int argc, VALUE *argv, VALUE self )
{
	SDL_Surface *surf;
	int x,y;
	int locked=0;
	Uint32 color;
	Uint8 *pixels, *pix;
	Uint8 r,g,b,a;

	Data_Get_Struct(self, SDL_Surface, surf);

	if(argc>2)
		rb_raise(rb_eArgError,"wrong number of arguments (%d for 1)",argc);

	if(argc==1)
	{
		x = NUM2INT(rb_ary_entry(argv[0],0));
		y = NUM2INT(rb_ary_entry(argv[0],1));
	}
	else
	{
		x = NUM2INT(argv[0]);
		y = NUM2INT(argv[1]);
	}

	if(x<0 || x>surf->w)
		rb_raise(rb_eIndexError,"x index out of bounds (%d, min 0, max %d)",\
			x,surf->w);
	if(y<0 || y>surf->h)
		rb_raise(rb_eIndexError,"y index out of bounds (%d, min 0, max %d)",\
			y,surf->h);

	/* lock surface */
	if(SDL_MUSTLOCK(surf))
	{
		if(SDL_LockSurface(surf)==0)
			locked += 1;
		else
			rb_raise(eSDLError,"could not lock surface: %s",SDL_GetError());
	}

/* borrowed from pygame */
	pixels = (Uint8 *) surf->pixels;

    switch(surf->format->BytesPerPixel)
    {
        case 1:
            color = (Uint32)*((Uint8 *)(pixels + y * surf->pitch) + x);
            break;
        case 2:
            color = (Uint32)*((Uint16 *)(pixels + y * surf->pitch) + x);
            break;
        case 3:
            pix = ((Uint8 *)(pixels + y * surf->pitch) + x * 3);
#if SDL_BYTEORDER == SDL_LIL_ENDIAN
            color = (pix[0]) + (pix[1]<<8) + (pix[2]<<16);
#else
            color = (pix[2]) + (pix[1]<<8) + (pix[0]<<16);
#endif
            break;
        default: /*case 4:*/
            color = *((Uint32*)(pixels + y * surf->pitch) + x);
            break;
	}

/* end borrowed from pygame */

	/* recursively unlock surface*/
	while(locked>1)
	{
		SDL_UnlockSurface(surf);
		locked -= 1;
	}

	if((int *)color == NULL)
	{
		VALUE zero = INT2NUM(0);
		return rb_ary_new3(4,zero,zero,zero,zero);
	}

	SDL_GetRGBA(color, surf->format, &r, &g, &b, &a);
	return rb_ary_new3(4,INT2NUM(r),INT2NUM(g),INT2NUM(b),INT2NUM(a));
}

/*
 *  call-seq:
 *    pixels  ->  String
 *
 *  Return a string of pixel data for the Surface. Most users will not
 *  need to use this method. If you want to convert a Surface into an
 *  OpenGL texture, pass the returned string to the TexImage2D method
 *  of the ruby-opengl library. (See samples/demo_gl_tex.rb for an example.)
 *
 *  (Please note that the dimensions of OpenGL textures must be powers of 2
 *  (e.g. 64x128, 512x512), so if you want to use a Surface as an OpenGL
 *  texture, the Surface's dimensions must also be powers of 2!)
 */
VALUE rbgm_surface_pixels( VALUE self )
{
	SDL_Surface *surf;
	Data_Get_Struct(self, SDL_Surface, surf);
	return rb_str_new(surf->pixels, (long)surf->pitch * surf->h);
}

void Rubygame_Init_Surface()
{

#if 0
	/* Pretend to define Rubygame module, so RDoc knows about it: */
	mRubygame = rb_define_module("Rubygame");
#endif

	cSurface = rb_define_class_under(mRubygame,"Surface",rb_cObject);
	rb_define_singleton_method(cSurface,"new",rbgm_surface_new,-1);
	rb_define_method(cSurface,"initialize",rbgm_surface_initialize,-1);
	rb_define_method(cSurface,"w",rbgm_surface_get_w,0);
	rb_define_alias(cSurface,"width","w");
	rb_define_method(cSurface,"h",rbgm_surface_get_h,0);
	rb_define_alias(cSurface,"height","h");
	rb_define_method(cSurface,"size",rbgm_surface_get_size,0);
	rb_define_method(cSurface,"depth",rbgm_surface_get_depth,0);
	rb_define_method(cSurface,"flags",rbgm_surface_get_flags,0);
	rb_define_method(cSurface,"masks",rbgm_surface_get_masks,0);
	rb_define_method(cSurface,"alpha",rbgm_surface_get_alpha,0);
	rb_define_method(cSurface,"set_alpha",rbgm_surface_set_alpha,-1);
	rb_define_method(cSurface,"colorkey",rbgm_surface_get_colorkey,0);
	rb_define_method(cSurface,"set_colorkey",rbgm_surface_set_colorkey,-1);
	rb_define_method(cSurface,"blit",rbgm_surface_blit,-1);
	rb_define_method(cSurface,"fill",rbgm_surface_fill,-1);
	rb_define_method(cSurface,"get_at",rbgm_surface_getat,-1);
  rb_define_method(cSurface,"pixels",rbgm_surface_pixels,0);
}
