#include <v_lib.h>

#define	MAX_GHOST		16
vboolean aa_enabled = false;

typedef struct particle_prop_s
{
	u32_t	num_ghosts;
	vec_t	ghost_size[MAX_GHOST];
}	particle_prop_t, *particle_prop_p;

typedef struct ps_frames_s
{
	char		title[256];
	u32_t		num_particles;
	u32_t		num_frames;
	u32_t		frame_delay;	/* 1000 / FPS */
	particle_prop_t	prop;
	bstack_t	props;
	vec3_t		*framedesc;
	vector_p	frame1, frame2;
	u32_t		nextframe;
	vboolean	loop;
	bstack_t	frames;
}	ps_frames_t, *ps_frames_p;

/*
 * ------------------------------------------------------------------------
 * Loading frame descriptions
 * ------------------------------------------------------------------------
 */
 
V_LOCAL vboolean
skip_brace(vhandle_t in,char skip)
{
	return lexSkipBlock(in,skip);
}

V_LOCAL vboolean check_next_token(vhandle_t in,char *check_with)
{
	return lexNextIs(in,check_with);
}

V_LOCAL vboolean check_last_token(vhandle_t in,char *check_with)
{
	return lexLastWas(in,check_with);
}

V_LOCAL vboolean next_int(vhandle_t in,s32_t *ret_val)
{
	return lexNextInteger(in,ret_val);
}

V_LOCAL vboolean last_int(vhandle_t in,s32_t *ret_val)
{
	return lexLastInteger(in,ret_val);
}

V_LOCAL vboolean next_float(vhandle_t in,vec_t *ret_val)
{
	return lexNextFloat(in,ret_val);
}

V_LOCAL vboolean
psyParseProp(vhandle_t in,particle_prop_t *prop)
{
	s32_t g_num, count;
	vec_t g_rad;
	if(!check_next_token(in,"{"))
		return false;
	for(count=0;count<MAX_GHOST;)
	{
		lexNext(in);
		if(!lexLast(in) || check_last_token(in,"}"))
			break;
		else if(check_last_token(in,"ghost"))
		{
			if(next_int(in,&g_num) && next_float(in,&g_rad) && 
				g_num == count)
			{
				prop->ghost_size[count] = g_rad;
				count++;
			}
		} else
		{
			conlPrintf(LOG_WARNING,
				"psyParseProp(line %d): unknown prop '%s'\n",
				lexCurrentLine(in),lexLast(in));
		}
	}
	prop->num_ghosts = (u32_t)count;
	if(!check_last_token(in,"}"))
		skip_brace(in,'}');
	if(!count)
	{
		conlPrintf(LOG_ERROR,
			"psyParseProp(line %d): empty property block.\n",
			lexCurrentLine(in));
		return false;
	}
	return true;
}

V_LOCAL vboolean
psyLoadProp(vhandle_t in,ps_frames_t *fr)
{
	u32_t i, propnum;
	bstack_t *props = &fr->props;
	if(!lexNext(in))
		return false;
	if(check_last_token(in,"common"))
	{
		if(!psyParseProp(in,&fr->prop))
			return false;
		if(!fr->num_particles)
		{
			conlPrintf(LOG_WARNING,"psyLoadProp(line %u): "
				"prop 'common' before token 'particles'\n",
				lexCurrentLine(in));
			stackPush(props,&fr->prop);
		}
		else
			for(i=stackCount(props);i<fr->num_particles;i++)
				stackPush(props,&fr->prop);
		return true;
	}
	if(!fr->num_particles || stackCount(props) < fr->num_particles)
		conlPrintf(LOG_WARNING,"psyLoadProp(line %u): "
			"prop override before prop 'common' or token 'particles'\n",
			lexCurrentLine(in));
	if(!last_int(in,(s32_t*)&propnum))
		return false;
	if(!psyParseProp(in,&fr->prop))
		return false;
	stackSet(props,propnum,&fr->prop);
	return true;
}

V_LOCAL vboolean next_vector(vhandle_t in,vec3_t out)
{
	vboolean ok;
	if(!check_next_token(in,"{"))
		return false;
	ok = next_float(in,out + 0) && check_next_token(in,",") &&
		next_float(in,out + 1) && check_next_token(in,",") &&
		next_float(in,out + 2);
	skip_brace(in,'}');
	return ok;
}

V_LOCAL vboolean
psyParseFrame(vhandle_t in,ps_frames_t *fr)
{
	u32_t count;
	vec3_t pos, vel;
	vboolean ok = false;
	if(!check_next_token(in,"{"))
		return false;
	for(count=0;count < fr->num_particles;count++)
	{
		ok = next_vector(in,pos)
			&& check_next_token(in,",")
			&& next_vector(in,vel);
		if(count < fr->num_particles-1)
			ok = ok && check_next_token(in,",");
		if(!ok && !check_last_token(in,"}"))
		{
			skip_brace(in,'}');
			return false;
		}
		vec3Copy(fr->framedesc[(count << 1)],pos);
		vec3Copy(fr->framedesc[(count << 1) + 1],vel);
	}
	skip_brace(in,'}');
	return true;
}

V_LOCAL vboolean
psyLoadFrame(vhandle_t in,ps_frames_t *fr)
{
	s32_t framenum = -1;
	vboolean got_framenum = next_int(in,&framenum);
	if(!got_framenum || framenum != (s32_t)fr->nextframe)
	{
		if(got_framenum)
			conlPrintf(LOG_ERROR,"psyLoadFrame(line %d): "
				"frame #%05d unexpected'\n",lexCurrentLine(in),
				framenum);
		if(skip_brace(in,'{'))
			skip_brace(in,'}');
		return false;
	}
	if(!psyParseFrame(in,fr))
		return false;
	stackSet(&fr->frames,fr->nextframe,fr->framedesc);
	fr->nextframe++;
	return true;
}

/*
 * Loads frame description from file 'fname'.
 * Returns filled 'ps_frames_p' structure which
 * must be later destroyed via psyDeleteFrames().
 */
ps_frames_p	V_GLOBAL
psyLoadFrames(char *fname)
{
	vhandle_t in;
	ps_frames_p fr;
	vboolean can_parse_frames = false, parse_error = false;
	in = lexParseFile(fname,"{},");
	if(in == LIBV_INVALID_HANDLE)
		return NULL;
	lexSetInteger(in,LEX_COMMENT_CHAR,'#');
	lexEnable(in,LEX_ONE_CHAR_COMMENTS);
	fr = (ps_frames_p)memAlloc(sizeof(ps_frames_t));
	memZero(fr,sizeof(ps_frames_t));
	stackInit(&fr->props,sizeof(particle_prop_t));
	while(lexNext(in))
	{
		if(check_last_token(in,"title"))
		{
			strCopy(fr->title,lexNext(in),256);
			continue;
		} else if(check_last_token(in,"particles"))
		{
			if(can_parse_frames)
			{
				conlPrintf(LOG_ERROR,
					"psyLoadFrames(%s:%d): parse error: unexpected '%s'.\n",
						fname,lexCurrentLine(in),lexLast(in));
			} else if(next_int(in,(s32_t*)&fr->num_particles))
			{
				u32_t framelen = fr->num_particles * 2 * sizeof(vec3_t);
				fr->frame1 = vecAlloc(3 * (fr->num_particles << 1));
				fr->frame2 = vecAlloc(3 * (fr->num_particles << 1));
				stackInit(&fr->frames,framelen);
				fr->framedesc = (vec3_t*)memAlloc(framelen);
				can_parse_frames = true;
				continue;
			}
		} else if(check_last_token(in,"frames"))
		{
			if(next_int(in,(s32_t*)&fr->num_frames))
				continue;
		} else if(check_last_token(in,"fps"))
		{
			vec_t fps;
			if(next_float(in,&fps) && fps > 0)
			{
				fr->frame_delay = (u32_t)(1000 / fps);
				continue;
			}
		} else if(check_last_token(in,"particle"))
		{
			if(psyLoadProp(in,fr))
				continue;
		} else if(check_last_token(in,"frame"))
		{
			if(can_parse_frames && psyLoadFrame(in,fr))
				continue;
		}
		conlPrintf(LOG_ERROR,
			"psyLoadFrames(%s): parse error in block (end on %d).\n",
				fname,lexCurrentLine(in));
		parse_error = true;
	}
	lexDestroy(in);
	if(!fr->num_particles || !fr->num_frames || !fr->frame_delay ||
		stackCount(&fr->props) != fr->num_particles)
	{
		conlPrintf(LOG_ERROR,
			"psyLoadFrames(%s): error(s) in source file.\n",fname);
		parse_error = true;
	}
	if(parse_error)
	{
		memFree(fr->frame1);
		memFree(fr->frame2);
		memFree(fr->framedesc);
		stackClear(&fr->props);
		stackClear(&fr->frames);
		memFree(fr);
		return NULL;
	}
	stackTrim(&fr->props);
	stackTrim(&fr->frames);
	return fr;
}

/*
 * Counterpart to psyLoadFrames()
 */
void	V_GLOBAL
psyDeleteFrames(ps_frames_p fr)
{
	memFree(fr->frame1);
	memFree(fr->frame2);
	memFree(fr->framedesc);
	stackClear(&fr->props);
	stackClear(&fr->frames);
	memFree(fr);
}

/*
 * Returns global parameters
 */
void	V_GLOBAL
psyGetParams(ps_frames_p fr,char **title,u32_t *nparticles_ret,
	u32_t *nframes_ret,u32_t *fdelay_ret)
{
	if(title)
		*title = fr->title;
	if(nparticles_ret)
		*nparticles_ret = fr->num_particles;
	if(nframes_ret)
		*nframes_ret = fr->num_frames;
	if(fdelay_ret)
		*fdelay_ret = fr->frame_delay;
}

/*
 * Returns static pointer to struct particle_prop_t or
 * NULL of 'prop_idx' > fr->num_particles.
 */
particle_prop_p	V_GLOBAL
psyGetProp(ps_frames_p fr,u32_t prop_idx)
{
	if(prop_idx >= fr->num_particles)
		return NULL;
	stackGet(&fr->props,&fr->prop,prop_idx);
	return &fr->prop;
}

/*
 * Moves pointer of next frame position to the beginning.
 */
void	V_GLOBAL
psyRewind(ps_frames_p fr)
{
	fr->nextframe = 0;
}

/*
 * Returns static pointer to array of vec3_t describing current frame.
 * Size of array is 2 * fr->num_particles; first vector contains position
 * of the 1st particle; second - velocity of 1st the particle, etc.
 */
vec3_t *psyNextFrame(ps_frames_p fr)
{
	stackGet(&fr->frames,fr->framedesc,fr->nextframe);
	if(fr->nextframe < fr->num_frames)
		fr->nextframe++;
	else
		if(fr->loop)
			fr->nextframe = 0;
	return fr->framedesc;
}

/*
 * Macro for accessing particle's position and velocity
 */
#define	PSY_FRAME_PARTICLE_PV(frame,j,p,v) \
	(p) = (frame)[(j) << 1]; \
	(v) = (frame)[((j) << 1) + 1];

/*
 * TODO: local second-order interpolation
 */
V_LOCAL void lerp_vectors(vec_t *dest,vector_t *v1,vector_t *v2,vec_t t)
{
	u32_t i;
	vec_t onet = 1 - t;
	for(i=0;i<v1->size;i++)
		dest[i] = onet * v1->elements[i] + t * v2->elements[i];
}

#if 0
void dframe(u32_t fn,u32_t np,vec3_t *frame)
{
	int j;
	vec_t *p, *v;
	printf("Frame %d\n",fn);
	for(j=0;j<np;j++)
	{
		PSY_FRAME_PARTICLE_PV(frame,j,p,v);
		printf("particle #%u:(%g,%g,%g)-(%g,%g,%g)\n",
			j,p[0],p[1],p[2],v[0],v[1],v[2]);
	}
}
#endif

/*
 * Returns static pointer to vec3_t array describing state of the
 * particle system at time 'time_s' seconds.
 * Size of vector is 2 * fr->num_particles; first vector contains position
 * of the 1st particle; second - velocity of 1st the particle, etc.
 */
vec3_t *psyFrameForTimeEx(ps_frames_p fr,vec_t time_s,vboolean loop,vec_t *time_used)
{
	s32_t f1, f2;
	vec_t between, t, frame_len, total_len, offset_from_start;
	/*
	 * fr->frame_delay contains frame length in milliseconds
	 */
	frame_len = (vec_t)0.001 * fr->frame_delay;
	total_len = frame_len * fr->num_frames;
	if(!loop)
	{
		vec_t max_time = total_len - frame_len;
		if(time_s <= 0)
			time_s = 0;
		else if(time_s >= max_time)
			time_s = max_time;
	}
	offset_from_start = fmod(time_s,total_len);
	if(offset_from_start < 0)
		offset_from_start += total_len;
	between = offset_from_start / frame_len;
	f1 = (u32_t)floor(between) % fr->num_frames;
	f2 = (f1 + 1) % fr->num_frames;
	t = between - f1;
	if(time_used)
		*time_used = offset_from_start;

#if 0
	printf("ctime %g;flen %g;tlen %g;ofs %g;btw %g;f1 %d;f2 %d;t %g\n",
		time_s,frame_len,total_len,offset_from_start,between,f1,f2,t);
#endif

	stackGet(&fr->frames,fr->frame1->elements,(u32_t)f1);
	stackGet(&fr->frames,fr->frame2->elements,(u32_t)f2);

#if 0
	dframe(f1,fr->num_particles,(vec3_t*)fr->frame2->elements);
	dframe(f2,fr->num_particles,(vec3_t*)fr->frame2->elements);
#endif

	lerp_vectors((vec_t*)fr->framedesc,fr->frame1,fr->frame2,t);
	return fr->framedesc;
}

/*
 * Same as above, uses 'true' as third parameter.
 */
vec3_t *psyFrameForTime(ps_frames_p fr,vec_t time_s)
{
	return psyFrameForTimeEx(fr,time_s,true,NULL);
}

#if 0

/*
 * Test suite
 */
void desc(ps_frames_p fr)
{
	u32_t i,j;
	vec3_t *frame;
	vec_t *p,*v;
	u32_t nf, np, fd;
	char *title;
	vec_t tm;
	psyGetParams(fr,&title,&np,&nf,&fd);
	printf("title '%s'\n",title);
	printf("particles %u\n",np);
	printf("frames %u\n",nf);
	printf("frame_delay %u\n",fd);

	for(tm=7.8;tm<8.5;tm+=0.2)
	{
		frame = psyFrameForTimeEx(fr,tm,true,&c_time);
		printf("Frame at %g\n",tm);
		for(j=0;j<fr->num_particles;j++)
		{
			PSY_FRAME_PARTICLE_PV(frame,j,p,v);
			printf("particle #%u:(%g,%g,%g)-(%g,%g,%g)\n",
				j,p[0],p[1],p[2],v[0],v[1],v[2]);
		}
	}

/*
	psyRewind(fr);
	for(i=0;i<nf;i++)
	{
		frame = psyNextFrame(fr);
		dframe(i,fr->num_particles,frame);
	}
*/
}

int main()
{
	ps_frames_p fr;
	logStartConsoleLog();
	logUpperLevel(LOG_ERROR);
	fr = psyLoadFrames("reference.psy");
	if(fr)
	{
		desc(fr);
		psyDeleteFrames(fr);
	}
	memPrintStats();
	return 0;
}

#endif

/*
 * -----------------------------------------------------------------------
 * Drawing stuff
 * -----------------------------------------------------------------------
 */
typedef struct drawstate_s
{
	vboolean spatial;
	vboolean animate;
	vboolean force_aa;
	u32_t astart;
	GLfloat rotza;
	GLfloat rotx, rotz;
	GLfloat zoom, vdx, vdy, vdz;
	vboolean inner_grid, outer_grid, ortho;
	int lod;
}	drawstate_t, *drawstate_p;

V_LOCAL drawstate_t drawstate;
V_LOCAL vhandle_t timer;
V_LOCAL	char *title;
V_LOCAL	vec3_t *cframe;
V_LOCAL	u32_t nparticles;
V_LOCAL	particle_prop_p prop;

V_LOCAL void draw_circle(GLfloat r,int segs)
{
	int i;
	GLfloat phi = 0;
	GLfloat dphi = (GLfloat)(M_2PI / segs);
	GLfloat x,y;
	glBegin(GL_LINE_LOOP);
	for(i=0;i<segs;i++,phi+=dphi)
	{
		x = r * (GLfloat)cos(phi);
		y = r * (GLfloat)sin(phi);
		glVertex2f(x,y);
	}
	glEnd();
}

V_LOCAL void draw_3circles(GLfloat r,int segs)
{
	draw_circle(r,segs);
	glRotatef(90,1,0,0);
	draw_circle(r,segs);
	glRotatef(90,0,1,0);
	draw_circle(r,segs);
}

#define	INNER_SEGS		16
#define	OUTER_SEGS		32

typedef struct draw_cspace_s
{
	GLfloat bk[4];	/* background */
	GLfloat og[4];	/* outer grid */
	GLfloat ig[4];	/* inner grid */
	GLfloat pt[4];	/* particle */
	GLfloat pg[4];	/* particle ghost */
	GLfloat fn[4];	/* font */
	GLfloat zb[4];	/* oZ straight lines */
}	draw_cspace_t, *draw_cspace_p;

V_LOCAL void
draw_particle(int lod,vec3_t pos,vec_t radius,vec_t r_minU,draw_cspace_p cs)
{
	glPushMatrix();
	glTranslated(pos[0],pos[1],pos[2]);
	if(lod > 0)
	{
		glColor4fv(cs->pt);
		draw_3circles((GLfloat)radius,INNER_SEGS);
	}
	if(lod > 1)
	{
		glColor4fv(cs->pg);
		draw_3circles((GLfloat)r_minU,OUTER_SEGS);
	}
	glPopMatrix();
}

V_LOCAL void
draw_xOy_grid(vec3_t origin,vec_t width,vec_t height,u32_t num_lines)
{
	u32_t i;
	vec3_t p, dp;
	vec3Copy(p,origin);
	p[0] -= (vec_t)0.5 * width;
	p[1] -= (vec_t)0.5 * height;
	vec3Set(dp,width,height,0);
	vec3Scale(dp,dp,(vec_t)1 / (num_lines-1));
	glBegin(GL_LINES);
		/* grid with lines along Oy direction */
		for(i=0;i<num_lines;i++)
		{
			VGL_VERTEX3(p[0]+i*dp[0],p[1],p[2]);
			VGL_VERTEX3(p[0]+i*dp[0],p[1]+height,p[2]);
		}
		/* grid with lines along Ox direction */
		for(i=0;i<num_lines;i++)
		{
			VGL_VERTEX3(p[0],p[1]+i*dp[1],p[2]);
			VGL_VERTEX3(p[0]+width,p[1]+i*dp[1],p[2]);
		}
	glEnd(); /* GL_LINES */
}

V_LOCAL void
draw_2_xOy_grids(vec3_t origin,vec3_t center,vec_t width,vec_t height,u32_t major_lines,u32_t minor_lines)
{
	vec_t dw, dh;
	vec3_t neworg;
	if(minor_lines < 2 || major_lines < 2)
		return;
	dw = width / (major_lines - 1);
	dh = height / (major_lines - 1);
	vec3Copy(neworg,center);
	neworg[0] -= (vec_t)fmod(center[0] - origin[0],dw);
	neworg[1] -= (vec_t)fmod(center[1] - origin[1],dh);
	if(aa_enabled)
		glLineWidth(1.5f);
	draw_xOy_grid(neworg,width,height,minor_lines);
	if(aa_enabled)
		glLineWidth(3);
	draw_xOy_grid(neworg,width,height,major_lines);
}

V_LOCAL void begin_overlay(int char_width, int char_height)
{
	glMatrixMode(GL_PROJECTION);
	glPushMatrix();
	glLoadIdentity();
	glOrtho(0,char_width,0,char_height,-1,1);
	glMatrixMode(GL_MODELVIEW);
	glPushMatrix();
	glLoadIdentity();
	VGL_TRANSLATE(0,char_height-1,0);
}

V_LOCAL void end_overlay(void)
{
	glMatrixMode(GL_PROJECTION);
	glPopMatrix();
	glMatrixMode(GL_MODELVIEW);
	glPopMatrix();
}

vec_t scale_base = 2;
vec_t view_scale, scale_exp, scale_mnt;

vec_t	c_time;
u32_t	rate_index = 4;
vec_t	rates[] = {0.01,0.03,0.1,0.3,1,3,10,30,100};
vec_t	time_dir = 1; /* from past to future */
vboolean paused = false;
vboolean loop = false;
vboolean fs = false;

V_LOCAL void draw_status()
{
	begin_overlay(40,30);
	vglPrintf(
		"%s\n"
		"Время %4.3g / %f\n"
		"1 дел. = %g\n",
		title,
		c_time,rates[rate_index],
		0.5 / view_scale); // fixme: (inner_size / inner_lines+1)
	end_overlay();
}

int lod = 1;

V_LOCAL void draw_reset_state()
{
	if(drawstate.spatial)
	{
		drawstate.rotx = -70;
		drawstate.rotz = 0;
		drawstate.zoom = (GLfloat)(1.1 * 1.1 * 1.1 * 1.1);
		drawstate.vdx = drawstate.vdy = drawstate.vdz = 0;
		drawstate.inner_grid = drawstate.outer_grid = true;
		drawstate.ortho = false;
		drawstate.lod = lod;
	}
	else
	{
		drawstate.rotx = drawstate.rotz = 0;
		drawstate.zoom = 1;
		drawstate.vdx = drawstate.vdy = drawstate.vdz = 0;
		drawstate.inner_grid = drawstate.outer_grid = true;
		drawstate.ortho = true;
		drawstate.lod = lod;
	}	
	drawstate.rotza = 0;
}

V_LOCAL void calc_bbox(vec3_t mins,vec3_t maxs)
{
	u32_t i;
	vec_t *p, *v;
	bboxClear(mins,maxs);
	for(i=0;i<nparticles;i++)
	{
		PSY_FRAME_PARTICLE_PV(cframe,i,p,v);
		bboxAddPoint(mins,maxs,p);
	}
}

V_LOCAL void mnt_and_exp(vec_t *mnt_ret,vec_t *exp_ret,vec_t x,vec_t base)
{
	vec_t sg;
	double ex, y;
	if(fabs(x) < 1e-50)
	{
		/* hack */
		if(mnt_ret)
			*mnt_ret = 1;
		if(exp_ret)
			*exp_ret = -50;
		return;
	}
	else
		if(x < 0)
		{
			sg = -1;
			x = -x;
		}
		else
			sg = 1;
	y = log10(x) / log10(base);
	ex = floor(y);
	if(exp_ret)
		*exp_ret = (vec_t)ex;
	if(mnt_ret)
		*mnt_ret = sg * (vec_t)pow(base,y - ex);
}

static draw_cspace_t default_cs =
{
	{0,   0,   0,   0.5f},	/* bk */
	{0.8f,0.6f,0,   0.5f},	/* og */
	{0.7f,1.0f,0,   0.6f},	/* ig */
	{1.0f,1.0f,1.0f,0.8f},	/* pt */
	{0.8f,0.0f,1.0f,0.7f},	/* pg */
	{1.0f,0.95f,0.93f,0.8f},	/* fn */
	{0.9f,0.0f,0.0f,0.6f}	/* zb */
};

static draw_cspace_t snapshot_cs =
{
	{1,   1,   1,   0.5f},	/* bk */
	{0.4f,0.0f,0,   0.3f},	/* og */
	{0.2f,0.6f,0,   0.6f},	/* ig */
	{0.0f,0.0f,0.0f,0.8f},	/* pt */
	{0.5f,0.0f,0.7f,0.7f},	/* pg */
	{0.4f,0.4f,0.0f,0.8f},	/* fn */
	{0.6f,0.0f,0.0f,0.6f}	/* zb */
};

V_LOCAL void draw(void *param)
{
	u32_t i;
	vec3_t mins, maxs, origin, center;
	vec_t *p, *v;
	draw_cspace_p cs = param ? (draw_cspace_p)param : &default_cs;
	
	glClearColor(cs->bk[0],cs->bk[1],cs->bk[2],cs->bk[3]);
	glClear(GL_COLOR_BUFFER_BIT);
	glPushMatrix();
	
    glRotatef(drawstate.rotx,1,0,0);
    glRotatef(drawstate.rotz + drawstate.rotza,0,0,1);

	/*
	 * Setup view transform from current positions
	 */
	calc_bbox(mins,maxs);
	vec3Zero(origin);
	vec3Sub(center,maxs,mins);
	view_scale = vec3Abs(center);
	if(view_scale != 0.0)
	{
		view_scale = 1.0 / view_scale;
	}
	else
		view_scale = 1e5;
	/*
	 * Round view_scale to nearest power of 'scale_base'
	 */
	mnt_and_exp(&scale_mnt,&scale_exp,view_scale,scale_base);
	view_scale = pow(scale_base,scale_exp);
	vec3Add(center,mins,maxs);
	center[2] = 0;
	vec3Scale(center,center,0.5f);
	VGL_SCALE(scale_mnt,scale_mnt,scale_mnt);
	VGL_TRANSLATE(-center[0]+drawstate.vdx,-center[1]+drawstate.vdy,-center[2]+drawstate.vdz);
	glScalef(drawstate.zoom,drawstate.zoom,drawstate.zoom);
	glScalef(drawstate.zoom,drawstate.zoom,drawstate.zoom);
	/*
	 * Draw coordsys-aligned grid on xOy plane
	 */
	vec3Set(origin,0,0,0);
	if(drawstate.outer_grid)
	{
		glColor4fv(cs->og);
		draw_2_xOy_grids(origin,center,8,8,3,5);
	}
	if(drawstate.inner_grid)
	{
		glColor4fv(cs->ig);
		draw_2_xOy_grids(origin,center,2,2,3,5);
	}
	VGL_SCALE(view_scale,view_scale,view_scale);
	/*
	 * Draw origin (FIXME: appears only on screen, not in snapshot)
	 */
	glColor4f(1,1,1,0.7f);
	glPointSize(6);
	glBegin(GL_POINTS);
		VGL_VERTEX3V(origin);
	glEnd();
	/*
	 * Draw particles
	 */
	if(drawstate.lod > 0)
	{
		if(aa_enabled)
			glLineWidth(1.5f);
		for(i=0;i<nparticles;i++)
		{
			PSY_FRAME_PARTICLE_PV(cframe,i,p,v);
			draw_particle(drawstate.lod,p,
				prop[i].ghost_size[0],prop[i].ghost_size[1],cs);
		}
	}
	else
	{
		glColor4fv(cs->pt);
		glBegin(GL_POINTS);
		for(i=0;i<nparticles;i++)
		{
			PSY_FRAME_PARTICLE_PV(cframe,i,p,v);
			VGL_VERTEX3V(p);
		}
		glEnd();
	}
	/* Ticks from particles to zero plane */
	if(drawstate.inner_grid || drawstate.outer_grid)
	{
		if(aa_enabled)
			glLineWidth(1.7f);
		glColor4fv(cs->zb);
		glBegin(GL_LINES);
			for(i=0;i<nparticles;i++)
			{
				PSY_FRAME_PARTICLE_PV(cframe,i,p,v);
				VGL_VERTEX3V(p);
				glVertex3f((GLfloat)p[0],(GLfloat)p[1],0);
			}
		glEnd();
	}
	/* Draw status */
	if(aa_enabled)
		glLineWidth(1.3f);
	glColor4fv(cs->fn);
	draw_status();
	/* Done */
	glPopMatrix();
	vglSwapBuffers();
	/*
	 * Animation: one revolution per minute
	 */
	if(drawstate.animate)
		drawstate.rotza = (timeTickCount() - drawstate.astart) * 0.006f;
}

V_LOCAL void V_CALL meta_draw(void *param)
{
	draw_cspace_p cs = (draw_cspace_p)param;
	glMatrixMode(GL_PROJECTION);
	glPushMatrix();
	glLoadIdentity();
	glOrtho(0,1,0,1,-1,1);
	glMatrixMode(GL_MODELVIEW);
	glPushMatrix();
	glLoadIdentity();
	glColor4fv(cs->bk);
	glRectf(-0.1f,-0.1f,1.1f,1.1f);
	glMatrixMode(GL_PROJECTION);
	glPopMatrix();
	glMatrixMode(GL_MODELVIEW);
	glPopMatrix();
	draw(param);
}

void ogl_setup_view(u32_t view_width,u32_t view_height,vboolean ortho,vboolean clip)
{
	GLfloat aspect;
	if(view_width && view_height)
	{
		glViewport(0,0,view_width,view_height);
		if(clip)
		{
			glScissor(0,0,view_width,view_height);
			glEnable(GL_SCISSOR_TEST);
		}
		else
			glDisable(GL_SCISSOR_TEST);
		aspect = (GLfloat)view_width / (GLfloat)view_height;
	}
	else
		aspect =  4.0f / 3.0f;
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	if(ortho)
		glOrtho(-aspect,aspect,-1,1,1,50);
	else
		glFrustum(-aspect,aspect,-1,1,1.5f,50);
	glMatrixMode(GL_MODELVIEW);
	glLoadIdentity();
	glTranslatef(0,0,-4);
}

vboolean ogl_setup(u32_t view_width,u32_t view_height,vboolean fullscreen)
{
	char *vendor, tmp[256];
	char *title=
		"Particle system viewer";
	if(fullscreen)
		vglCreateFullscreenWindow(title,view_width,view_height,0,0);
	else
		vglCreateWindow(title,0,0,view_width,view_height);
	if(vglGetCurrentWindow()==LIBV_INVALID_HANDLE)
		return false;
	/*
	 * We enable AA lines only on NV chips
	 */
	vendor = (char*)glGetString(GL_VENDOR);
	strCopy(tmp,vendor,256);
	strLower(tmp);
	if(strstr(tmp,"nvidia") || drawstate.force_aa)
		aa_enabled = true;
	vglLoadDefaultFont();
	ogl_setup_view(view_width,view_height,drawstate.ortho,false);
	/* Current matrix mode is MODELVIEW */
	if(aa_enabled)
	{
		glEnable(GL_BLEND);
		glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA);
		glEnable(GL_LINE_SMOOTH);
		glEnable(GL_POINT_SMOOTH);
	}
	return true;
}

void ogl_shutdown(void)
{
	vglDestroyWindow(vglGetCurrentWindow());
}

u32_t	shot_count = 0;

void draw_handle_keydown(u32_t keycode)
{
	char wmf_name[_MAX_PATH];
	u32_t ridx = rate_index;
	switch(keycode)
	{
		case key_up:
			drawstate.rotx += 10;
			break;
		case key_down:
			drawstate.rotx -= 10;
			break;
		case key_left:
			drawstate.rotz += 10;
			break;
		case key_right:
			drawstate.rotz -= 10;
			break;
		case key_kp_4:
			drawstate.vdx += 1;
			break;
		case key_kp_6:
			drawstate.vdx -= 1;
			break;
		case key_kp_2:
			drawstate.vdy += 1;
			break;
		case key_kp_8:
			drawstate.vdy -= 1;
			break;
		case key_kp_7:
			drawstate.vdz += 1;
			break;
		case key_kp_9:
			drawstate.vdz -= 1;
			break;
		case key_kp_minus:
			drawstate.zoom /= (GLfloat)1.1;
			break;
		case key_kp_plus:
			drawstate.zoom *= (GLfloat)1.1;
			break;
		case key_R:
			timerStop(timer);
			timerStart(timer);
			if(paused)
				timerStop(timer);
			break;
		case key_U:
			paused = !paused;
			if(!paused)
				timerResume(timer);
			else
				timerStop(timer);
			break;
		case key_1:
			rate_index = 0;
			break;
		case key_2:
			rate_index = 1;
			break;
		case key_3:
			rate_index = 2;
			break;
		case key_4:
			rate_index = 3;
			break;
		case key_5:
			rate_index = 4;
			break;
		case key_6:
			rate_index = 5;
			break;
		case key_7:
			rate_index = 6;
			break;
		case key_8:
			rate_index = 7;
			break;
		case key_9:
			rate_index = 8;
		case key_0:
			time_dir = time_dir * -1;
			timerRate(timer,rates[rate_index] * time_dir);
			break;
		case key_A:
			drawstate.animate = !drawstate.animate;
			if(drawstate.animate)
				drawstate.astart = timeTickCount();
			else
				drawstate.rotza = 0;
			break;
		case key_I:
			drawstate.inner_grid = !drawstate.inner_grid;
			break;
		case key_O:
			drawstate.outer_grid = !drawstate.outer_grid;
			break;
		case key_D:
			drawstate.lod = (drawstate.lod + 1) % 3;
			break;
		case key_P:
			drawstate.ortho = !drawstate.ortho;
			ogl_setup_view(0,0,drawstate.ortho,false);
			break;
		case key_space:
			draw_reset_state();
			ogl_setup_view(0,0,drawstate.ortho,false);
			break;
		case key_kp_5:
			drawstate.zoom *= 0.1f;
			break;
		case key_S:
			/*
			 * Snapshot
			 */
			if(!paused)
				timerStop(timer);
			logFormatString(wmf_name,"shot%04u.wmf",shot_count++);
			vglDrawToMetafile(wmf_name,133,100,meta_draw,&snapshot_cs);
			vglDrawToMetafile(libv_clipboard_name,133,100,meta_draw,&snapshot_cs);
			if(!paused)
				timerResume(timer);
			break;
		default:
			break;
	}
	if(ridx != rate_index)
		timerRate(timer,rates[rate_index] * time_dir);
}

void draw_handle_keyup(u32_t keycode)
{
	switch(keycode)
	{
		case key_kp_5:
			drawstate.zoom *= 10;
			break;
		default:
			break;
	}
}

/*
 * -----------------------------------------------------------------------
 * Part 5: UI
 */
vboolean ui_handle_event(input_record_t *in)
{
	switch(in->event)
	{
		case ev_key_down:
			if(in->first_code == key_escape)
				return false;
			draw_handle_keydown(in->first_code);
			break;
		case ev_key_up:
			draw_handle_keyup(in->first_code);
			break;
		default:
			break;
	}
	return true;
}

void ui_help(void)
{
	conPuts(
		"Interactive mode keys:\n"
		"Escape - exit;\n"
		"Cursor keys - rotation;\n"
		"Numpad keys (4/6;2/8;7/9) - shift in object space;\n"
		"Numpad +/- - zoom in/out;\n"
		"Numpad 5 - temporary zoom out;\n"
		"'I','O' - toggle inner/outer grids;\n"
		"'D' - toggle particle level of detail.\n");
}

#define	MAX_FPS			100
V_LOCAL ps_frames_p		fr = NULL;

void ui_main_loop(void)
{
	input_record_t in;
	u32_t idraw, delay_ms = 1000 / MAX_FPS;
	timer = timerCreate();
	timerStart(timer);
	for(;;)
	{
		idraw = timeTickCount();
		/*
		 * Process all UI events
		 */
		while(vglPending())
		{
			vglNextEvent(&in);
			if(!ui_handle_event(&in))
				goto good_bye;
		}
		/*
		 * Load next frame
		 */
		cframe = psyFrameForTimeEx(fr,timerTime(timer),loop,&c_time);
		/*
		 * Draw
		 */
		draw(&default_cs);
		/*
		 * Delay
		 */
		idraw = timeTickCount() - idraw;
		if(idraw < delay_ms)
			timeDelay(delay_ms - idraw);
	}
good_bye:;
	timerDestroy(timer);
}

char *infile = NULL;

typedef struct cmdvar_s
{
	char	*id;
	char	*desc;
	int		typeid; /* 0 - boolean / integer; 1 - float; 2 - string, 3 - last var in list */
	void	*value;
}	cmdvar_t, *cmdvar_p;

static cmdvar_t cmds[] =
{
	{"-fs","fullscreen mode (0/1)",0,&fs},
	{"-loop","infinite loop (0/1)",0,&loop},
	{"-lod","particle LOD (0/1/2)",0,&lod},
	{"-animate","constant rotation (0/1)",0,&drawstate.animate},
	{"-spatial","perspective view (0/1)",0,&drawstate.spatial},
	{"-aa","force AA lines on non-nVidia GPUs (0/1)",0,&drawstate.force_aa},
	{"","",3,NULL}
};

vboolean parse_cmdline(int argc,char *argv[])
{
	int i,j;
	for(i=1;i<argc;i++)
	{
		if(argv[i][0]!='-')
		{
			infile = argv[i];
			continue;
		}
		for(j=0;cmds[j].typeid<3;j++)
		{
			if(!strcmp(argv[i],cmds[j].id))
			{
				if(i >= argc-1)
				{
					printf("error: option '%s' needs %s argument.\n",argv[i],
						cmds[j].typeid ? (cmds[j].typeid == 2 ? "string" : "float") : "integer");
					return false;
				}
				switch(cmds[j].typeid)
				{
					case 0:
						*((int*)cmds[j].value) = atoi(argv[++i]);
						printf("%s=%d\n",cmds[j].id,*((int*)cmds[j].value));
						break;
					case 1:
						*((vec_t*)cmds[j].value) = (vec_t)atof(argv[++i]);
						printf("%s=%g\n",cmds[j].id,*((vec_t*)cmds[j].value));
						break;
					case 2:
						*((char**)cmds[j].value) = argv[++i];
						printf("%s=%s\n",cmds[j].id,*((char**)cmds[j].value));
						break;
				}
				break;
			}
		}
		if(cmds[j].typeid == 3)
			printf("unknown option: '%s'\n",argv[i]);
	}
	return true;
}
	
/*
 * main()
 */
int main(int argc,char *argv[])
{
	logStartConsoleLog();
	logUpperLevel(LOG_TRACE);
	parse_cmdline(argc,argv);
	draw_reset_state();
#ifndef	__WIN_GUI
	ui_help();
#endif
	/* fsInit(); */
	fr = infile ? psyLoadFrames(infile) : psyLoadFrames("reference.psy");
	if(!fr)
		return -1;
	psyGetParams(fr,&title,&nparticles,NULL,NULL);
	prop = (particle_prop_t*)stackGetStorage(&fr->props);
	/*
	vec3_t mins, maxs;
	cframe = psyFrameForTime(fr,0);
	calc_bbox(mins,maxs);
	drawstate.spatial = !IsZero(v_abs(maxs[2] - mins[2]));
	*/
	if(fs)
	{
		if(!ogl_setup(0,0,true))
			return -1;
	}
	else
	{
		if(!ogl_setup(800,600,false))
			return -1;
	}
	ui_main_loop();
	ogl_shutdown();
	psyDeleteFrames(fr);
	memPrintStats();
	logStopConsoleLog();
	return 0;
}

#ifdef	__WIN_GUI
long __stdcall WinMain(void *arg1,void *arg2,char *cmdline,int cmdshow)
{
	int argc, retval;
	char *argv[1024] = {"WinMain()"};
	vhandle_t scan = lexParseString(cmdline,NULL);
	for(argc=1;argc<1024 && lexNext(scan);)
		argv[argc++] = strDup(lexLast(scan));
	lexDestroy(scan);
	retval = main(argc,argv);
	while(argc)
		memFree(argv[argc--]);
	return (long)retval;
}
#endif	/* __WIN_GUI */
