/*
	
	StarWindow.cpp
	
	by Pierre Raynaud-Richard.
	
*/

/*
	Copyright 1999, Be Incorporated.   All Rights Reserved.
	This file may be used under the terms of the Be Sample Code License.
*/

#include <Application.h>
#include <View.h>
#include "StarWindow.h"
#include "Stars.h"

#include <string.h>
#include <stdlib.h>

#include <AppFileInfo.h>
#include <FindDirectory.h>
#include <Alert.h>
#include <File.h>
#include <Path.h>

#include <Debug.h>

// return the version_info of a file, described by its
// name and its generic folder (in find_directory syntax).
status_t get_file_version_info(	directory_which	dir,
								char			*filename,
								version_info	*info) {
	BPath 			path;
	BFile			file;
	status_t		res;
	BAppFileInfo	appinfo;

	// find the directory
	if ((res = find_directory(dir, &path)) != B_NO_ERROR)
		return res;

	// find the file
	path.Append(filename);
	file.SetTo(path.Path(), O_RDONLY);
	if ((res = file.InitCheck()) != B_NO_ERROR)
		return res;

	// get the version_info
	if ((res = appinfo.SetTo(&file)) != B_NO_ERROR)
		return res;
	return appinfo.GetVersionInfo(info, B_APP_VERSION_KIND);
}

enum {
	// pseudo-random generator parameters (not very good ones,
	// but good enough for what we do here).
	CRC_START		= 0x56dec231,
	CRC_KEY			= 0x1789feb3
};

StarWindow::StarWindow(BRect frame, const char *name)
: BDirectWindow(frame, name, B_TITLED_WINDOW, 0)
{
	BView			*view;
	uint32			i;
	int32			x, y, dx, dy, cnt, square;
	version_info	vi;

	// check if we need the special trick to work around the bug in
	// the R3 directwindow protocol. This bug only affects the
	// B_BUFFER_RESET flag : the app_server forget to set it when
	// showing a previously hidden window. If you don't care about
	// the B_BUFFER_RESET flag, you're not concerned by that work
	// around.
	need_r3_buffer_reset_work_around = false;
	if (get_file_version_info(B_BEOS_SERVERS_DIRECTORY, "app_server", &vi) == B_NO_ERROR)
		if ((vi.major == 1) && (vi.middle == 3) && (vi.minor == 0))
			need_r3_buffer_reset_work_around = true;

	// init the crc pseudo-random generator
	crc_alea = CRC_START;
	
	// allocate the star struct array
	star_count_max = 8192;
	star_count = 0;
	star_list = (star*)malloc(sizeof(star)*star_count_max);

	// initialise the default state of the star array
	for (i=0; i<star_count_max; i++) {
	
		// peek a random vector. This is certainly not the nicest way
		// to do it (the probability and the angle are linked), but that's
		// simple and doesn't require any trigonometry.
		do {
			dx = (crc_alea&0xffff) - 0x8000;		
			CrcStep();
			CrcStep();
				
			dy = (crc_alea&0xffff) - 0x8000;			
			CrcStep();
			CrcStep();
		} while ((dx == 0) && (dy == 0));
		
		// enforce a minimal length by doubling the vector as many times
		// as needed.
		square = dx*dx+dy*dy;
		while (square < 0x08000000) {
			dx <<= 1;
			dy <<= 1;
			square <<= 2;
		}
			
		// save the starting speed vector.
		star_list[i].dx0 = dx;
		star_list[i].dy0 = dy;
		
		// simulate the animation to see how many moves are needed to
		// get out by at least 1024 in one direction. That will give us
		// an minimal value for how long we should wait before restarting
		// the animation. It wouldn't work if the window was getting
		// much larger than 2048 pixels in one dimension.
		cnt = 0;
		x = 0;
		y = 0;
		while ((x<0x4000000) && (x>-0x4000000) && (y<0x4000000) && (y>-0x4000000)) {
			x += dx;
			y += dy;
			dx += (dx>>4);
			dy += (dy>>4);
			cnt++;
		}
		
		// add a random compenent [0 to 15] to the minimal count before
		// restart.
		star_list[i].count0 = cnt + ((crc_alea&0xf0000)>>16);
		// make the star initialy invisible and fixed, then spread the
		// value of their restart countdown so that they won't start all
		// at the same time, but progressively
		star_list[i].last_draw = INVALID;
		star_list[i].x = 0x40000000;
		star_list[i].y = 0x40000000;
		star_list[i].dx = 0;
		star_list[i].dy = 0;
		star_list[i].count = (i&255);
	}	

	// allocate the semaphore used to synchronise the star animation drawing access.
	drawing_lock = create_sem(0, "star locker");

	// spawn the star animation thread (we have better set the force quit flag to
	// false first).
	kill_my_thread = false;
	my_thread = spawn_thread(StarWindow::StarAnimation, "StarAnimation",
							 B_DISPLAY_PRIORITY, (void*)this);
	resume_thread(my_thread);
	
	// add a view in the background to insure that the content area will
	// be properly erased in black. This erase mechanism is not synchronised
	// with the star animaton, which means that from time to time, some
	// stars will be erreneously erased by the view redraw. But as every
	// single star is erased and redraw every frame, that graphic glitch
	// will last less than a frame, and that just in the area being redraw
	// because of a window resize, move... Which means the glitch won't
	// be noticeable. The other solution for erasing the background would
	// have been to do it on our own (which means some serious region
	// calculation and handling all color_space). Better to use the kit
	// to do it, as it gives us access to hardware acceleration...
	frame.OffsetTo(0.0, 0.0);
	view = new BView(frame, "", B_FOLLOW_ALL, B_WILL_DRAW);
	
	// The only think we want from the view mechanism is to
	// erase the background in black. Because of the way the
	// star animation is done, this erasing operation doesn't
	// need to be synchronous with the animation. That the
	// reason why we use both the direct access and the view
	// mechanism to draw in the same area of the StarWindow.
	// Such thing is usualy not recommended as synchronisation
	// is generally an issue (drawing in random order usualy
	// gives remanent incorrect result).	
	// set the view color to be black (nicer update).
	view->SetViewColor(0, 0, 0);
	AddChild(view);
	
	// Add a shortcut to switch in and out of fullscreen mode.
	AddShortcut('f', B_COMMAND_KEY, new BMessage('full'));	
	
	// As we said before, the window shouldn't get wider than 2048 in any
	// direction, so those limits will do.
	SetSizeLimits(40.0, 2000.0, 40.0, 2000.0);
	
	// If the graphic card/graphic driver we use doesn't support directwindow
	// in window mode, then we need to switch to fullscreen immediatly, or
	// the user won't see anything, as long as it doesn't used the undocumented
	// shortcut. That would be bad behavior...
	if (!BDirectWindow::SupportsWindowMode()) {
		bool		sSwapped;
		char		*buf;
		BAlert		*quit_alert;
		key_map		*map;
		
		get_key_map(&map, &buf);
		sSwapped = (map->left_control_key==0x5d) && (map->left_command_key==0x5c);
		free(map);
		free(buf);
		quit_alert = new BAlert("QuitAlert", sSwapped ?
		                        "This demo runs only in full screen mode.\n"
		                        "While running, press 'Ctrl-Q' to quit.":
		                        "This demo runs only in full screen mode.\n"
		                        "While running, press 'Alt-Q' to quit.",
		                        "Quit", "Start demo", NULL,
		                        B_WIDTH_AS_USUAL, B_WARNING_ALERT);
		if (quit_alert->Go() == 0)
			((StarsApp*)be_app)->abort_required = true;
		else
			SetFullScreen(true);
	}
}

StarWindow::~StarWindow()
{
	long		result;

	// force the drawing_thread to quit. This is the easiest way to deal
	// with potential closing problem. When it's not practical, we
	// recommand to use Hide() and Sync() to force the disconnection of
	// the direct access, and use some other flag to guarantee that your
	// drawing thread won't draw anymore. After that, you can pursue the
	// window destructor and kill your drawing thread...
	kill_my_thread = true;
	delete_sem(drawing_lock);
	wait_for_thread(my_thread, &result);
	
	// Free window resources. As they're used by the drawing thread, we
	// need to terminate that thread before freeing them, or we could crash.
	free(star_list);
}

bool StarWindow::QuitRequested()
{
	be_app->PostMessage(B_QUIT_REQUESTED);
	return(TRUE);
}

void StarWindow::MessageReceived(BMessage *message)
{
	int8		key_code;

	switch(message->what) {
	// Switch between full-screen mode and windowed mode.
	case 'full' :
		SetFullScreen(!IsFullScreen());
		break;
	case B_KEY_DOWN :
		if (!IsFullScreen())
			break;
		if (message->FindInt8("byte", &key_code) != B_OK)
			break;
		if (key_code == B_ESCAPE)
			PostMessage(B_QUIT_REQUESTED);
		break;
	default :
		BDirectWindow::MessageReceived(message);
		break;
	}
}

void StarWindow::CrcStep()
{
	// basic crc pseudo-random generator
	crc_alea <<= 1;
	if (crc_alea < 0)
		crc_alea ^= CRC_KEY;
}

void StarWindow::DirectConnected(direct_buffer_info *info)
{
	// you need to use that mask to read the buffer state.
	switch (info->buffer_state & B_DIRECT_MODE_MASK) {
	// start a direct screen connection.
	case B_DIRECT_START :
		SwitchContext(info);	// update the direct screen infos.
		release_sem(drawing_lock);	// unblock the animation thread.
		break;
	// stop a direct screen connection.
	case B_DIRECT_STOP :
		acquire_sem(drawing_lock);	// block the animation thread.
		break;
	// modify the state of a direct screen connection.
	case B_DIRECT_MODIFY :
		acquire_sem(drawing_lock);	// block the animation thread.
		SwitchContext(info);	// update the direct screen infos.
		release_sem(drawing_lock);	// unblock the animation thread.
		break;
	default :
		break;
	}
}

// This function update the internal graphic context of the StarWindow
// object to reflect the infos send through the DirectConnected API.
// It also update the state of stars (and erase some of them) to
// insure a clean transition during resize. As this function is called
// in DirectConnected, it's a bad idea to do any heavy drawing (long)
// operation. But as we only update the stars (the background will be
// updated a little later by the view system), it's not a big deal.
void StarWindow::SwitchContext(direct_buffer_info *info)
{
	star			*s;
	int32			x, y, deltax, deltay;
	uint32			i, j, window_area, cx, cy;
	uint32			star_count_new;
	clipping_rect	*r;

	// calculate the new star count, depending the size of the window frame.
	// we do that because we want to keep the star count proportionnal to
	// the size of the window, to keep an similar overall star density feeling
	window_area = (info->window_bounds.right-info->window_bounds.left+1)*
				  (info->window_bounds.bottom-info->window_bounds.top+1);
	// max out beyond 1M pixels.
	if (window_area > (1<<20))
		window_area = (1<<20);
	star_count_new = (star_count_max*(window_area>>10))>>10;
	if (star_count_new > star_count_max)
		star_count_new = star_count_max;
		
	// set the position of the new center of the window (in case of move or resize)
	cx = (info->window_bounds.right+info->window_bounds.left+1)/2;
	cy = (info->window_bounds.bottom+info->window_bounds.top+1)/2;
	
	// update to the new clipping region. The local copy is kept relative
	// to the center of the animation (origin of the star coordinate).
	clipping_bound.left = info->clip_bounds.left - cx;
	clipping_bound.right = info->clip_bounds.right - cx;
	clipping_bound.top = info->clip_bounds.top - cy;
	clipping_bound.bottom = info->clip_bounds.bottom - cy;
	// the clipping count is bounded (see comment in header file).
	clipping_list_count = info->clip_list_count;
	if (clipping_list_count > MAX_CLIPPING_RECT_COUNT)
		clipping_list_count = MAX_CLIPPING_RECT_COUNT;
	for (i=0; i<clipping_list_count; i++) {
		clipping_list[i].left = info->clip_list[i].left - cx;
		clipping_list[i].right = info->clip_list[i].right - cx;
		clipping_list[i].top = info->clip_list[i].top - cy;
		clipping_list[i].bottom = info->clip_list[i].bottom - cy;
	}
	
	// update the new rowbyte
	row_bytes = info->bytes_per_row/(info->bits_per_pixel/8);
	
	// update the screen bases (only one of the 3 will be really used).
	draw_ptr8 = ((uint8*)info->bits) + row_bytes*info->window_bounds.top + info->window_bounds.left;
	draw_ptr16 = ((uint16*)info->bits) + row_bytes*info->window_bounds.top + info->window_bounds.left;
	draw_ptr32 = ((uint32*)info->bits) + row_bytes*info->window_bounds.top + info->window_bounds.left;

	// cancel the erasing of all stars if the buffer has been reset.
	// Because of a bug in the R3 direct window protocol, B_BUFFER_RESET is not set
	// whew showing a previously hidden window. The second test is a reasonnable
	// way to work around that bug...
	if ((info->buffer_state & B_BUFFER_RESET) ||
		(need_r3_buffer_reset_work_around &&
		 ((info->buffer_state & (B_DIRECT_MODE_MASK|B_BUFFER_MOVED)) == B_DIRECT_START))) {
		s = star_list;
		for (i=0; i<star_count_max; i++) {
			s->last_draw = INVALID;
			s++;
		}
	}
	// in the other case, update the stars that will stay visible.
	else {
		// calculate the delta vector due to window resize or move.
		deltax = cx_old - (cx - info->window_bounds.left);
		deltay = cy_old - (cy - info->window_bounds.top);
		// check all the stars previously used.
		s = star_list;
		for (i=0; i<star_count; i++) {
			// if the star wasn't visible before, then no more question.
			if (s->last_draw == INVALID)
				goto not_defined;
			// convert the old position into the new referential.
			x = (s->x>>16) + deltax;
			y = (s->y>>16) + deltay;
			// check if the old position is still visible in the new clipping
			if ((x < clipping_bound.left) || (x > clipping_bound.right) ||
				(y < clipping_bound.top) || (y > clipping_bound.bottom))
				goto invisible;
			if (clipping_list_count == 1)
				goto visible;
			r = clipping_list;
			for (j=0; j<clipping_list_count; j++) {
				if ((x >= r->left) && (x <= r->right) &&
					(y >= r->top) && (y <= r->bottom))
					goto visible;
				r++;
			}
			goto invisible;
			
			// if it's still visible...
		visible:
			if (i >= star_count_new) {
				// ...and the star won't be used anylonger, then we erase it. 
				if (pixel_depth == 32)
					draw_ptr32[s->last_draw] = 0;
				else if (pixel_depth == 16)
					draw_ptr16[s->last_draw] = 0;
				else
					draw_ptr8[s->last_draw] = 0;
			}
			goto not_defined;
				
			// if the star just became invisible and it was because the
			// context was modified and not fully stop, then we need to erase
			// those stars who just became invisible (or they could leave
			// artefacts in the drawing area in some cases). This problem is
			// a side effect of the interaction between a complex resizing
			// case (using 2 B_DIRECT_MODIFY per step), and the dynamic
			// star count management we are doing. In most case, you never
			// have to erase things going out of the clipping region...
		invisible:
			if ((info->buffer_state & B_DIRECT_MODE_MASK) == B_DIRECT_MODIFY) {
				if (pixel_depth == 32)
					draw_ptr32[s->last_draw] = 0;
				else if (pixel_depth == 16)
					draw_ptr16[s->last_draw] = 0;
				else
					draw_ptr8[s->last_draw] = 0;
			}
			// and set its last position as invalid.
			s->last_draw = INVALID;
		not_defined:
			s++;
		}
		
		// initialise all the new star (the ones which weren't used but
		// will be use after that context update) to set their last position
		// as invalid.
		s = star_list+star_count;
		for (i=star_count; i<star_count_new; i++) {
			s->last_draw = INVALID;
			s++;
		}
	}
	
	// update the window origin offset.
	window_offset = row_bytes*(cy-info->window_bounds.top) + (cx-info->window_bounds.left);
	
	// set the pixel_depth and the pixel data, from the color_space.
	switch (info->pixel_format) {
	case B_RGBA32 :
	case B_RGB32 :
		pixel_depth = 32;
		((uint8*)&pixel32)[0] = 0x20; 
		((uint8*)&pixel32)[1] = 0xff; 
		((uint8*)&pixel32)[2] = 0x20; 
		((uint8*)&pixel32)[3] = 0xff;
		break;
	case B_RGB16 :
		pixel_depth = 16;
		((uint8*)&pixel16)[0] = 0xe0;
		((uint8*)&pixel16)[1] = 0x07;
		break;
	case B_RGB15 :
	case B_RGBA15 :
		pixel_depth = 16;
		((uint8*)&pixel16)[0] = 0xe0;
		((uint8*)&pixel16)[1] = 0x03;
		break;
	case B_CMAP8 :
		pixel_depth = 8;
		pixel8 = 52;
		break;
	case B_RGBA32_BIG :
	case B_RGB32_BIG :
		pixel_depth = 32;
		((uint8*)&pixel32)[3] = 0x20; 
		((uint8*)&pixel32)[2] = 0xff; 
		((uint8*)&pixel32)[1] = 0x20; 
		((uint8*)&pixel32)[0] = 0xff;
		break;
	case B_RGB16_BIG :
		pixel_depth = 16;
		((uint8*)&pixel16)[1] = 0xe0;
		((uint8*)&pixel16)[0] = 0x07;
		break;
	case B_RGB15_BIG :
	case B_RGBA15_BIG :
		pixel_depth = 16;
		((uint8*)&pixel16)[1] = 0xe0;
		((uint8*)&pixel16)[0] = 0x03;
		break;
	default:	// unsupported color space?
		fprintf(stderr, "ERROR - unsupported color space!\n");
		exit(1);
		break;
	}
	
	// set the new star count.
	star_count = star_count_new;
	
	// save a copy of the variables used to calculate the move of the window center
	cx_old = cx - info->window_bounds.left;
	cy_old = cy - info->window_bounds.top;
}

// This is the thread doing the star animation itself. It would be easy to
// adapt to do any other sort of pixel animation.
long StarWindow::StarAnimation(void *data)
{
	star			*s;
	int32			x, y;
	uint32			i, j;
	bigtime_t		time;
	StarWindow		*w;
	clipping_rect	*r;
	
	// receive a pointer to the StarWindow object.
	w = (StarWindow*)data;
	
	// loop, frame after frame, until asked to quit.
	while (!w->kill_my_thread) {
	
		// we want a frame to take at least 16 ms.
		time = system_time()+16000;
		
		// get the right to do direct screen access.
		acquire_sem(w->drawing_lock);
		if (w->kill_my_thread) break;
		
		// go through the array of star, for all currently used star.
		s = w->star_list;
		for (i=0; i<w->star_count; i++) {
			if (s->count == 0) {
				// restart the star animation, from a random point close to
				// the center [-16, +15], both axis.
				x = s->x = ((w->crc_alea&0x1f00)>>8) - 16;
				y = s->y = ((w->crc_alea&0x1f0000)>>16) - 16;
				s->dx = s->dx0;
				s->dy = s->dy0;
				// add a small random component to the duration of the star
				s->count = s->count0 + (w->crc_alea&0x7);
				w->CrcStep();
			}
			else {
				// just move the star
				s->count--;
				x = s->x += s->dx;
				y = s->y += s->dy;
				s->dx += (s->dx>>4);
				s->dy += (s->dy>>4);
			}
			// erase the previous position, if necessary
			if (s->last_draw != INVALID) {
				if (w->pixel_depth == 32)
					w->draw_ptr32[s->last_draw] = 0;
				else if (w->pixel_depth == 16)
					w->draw_ptr16[s->last_draw] = 0;
				else
					w->draw_ptr8[s->last_draw] = 0;
			}
			// check if the new position is visible in the current clipping
			x >>= 16;
			y >>= 16;
			if ((x < w->clipping_bound.left) || (x > w->clipping_bound.right) ||
				(y < w->clipping_bound.top) || (y > w->clipping_bound.bottom))
				goto invisible;
			if (w->clipping_list_count == 1) {
		visible:
				// if it's visible, then draw it.
				s->last_draw = w->window_offset + w->row_bytes*y + x;
				if (w->pixel_depth == 32)
					w->draw_ptr32[s->last_draw] = w->pixel32;
				else if (w->pixel_depth == 16)
					w->draw_ptr16[s->last_draw] = w->pixel16;
				else
					w->draw_ptr8[s->last_draw] = w->pixel8;
				goto loop;
			}
			// handle complex clipping cases
			r = w->clipping_list;
			for (j=0; j<w->clipping_list_count; j++) {
				if ((x >= r->left) && (x <= r->right) &&
					(y >= r->top) && (y <= r->bottom))
					goto visible;
				r++;
			}
		invisible:
			// if not visible, register the fact that the star wasn't draw.
			s->last_draw = INVALID;
		loop:
			s++;
		}		
		
		// release the direct screen access
		release_sem(w->drawing_lock);
		
		// snooze for whatever time is left from the initial allocation done
		// at the beginning of the loop.
		time -= system_time();
		if (time > 0)
			snooze(time);
	}	
	return 0;
}

