#include <unistd.h>
#include <sys/types.h>
#include <sys/time.h>
#include <sys/resource.h>
#include <sys/wait.h>

#include "QF/dstring.h"
#include "QF/quakeio.h"
#include "QF/sys.h"

#include "QuakeEd.h"
#include "Clipper.h"
#include "XYView.h"
#include "Map.h"
#include "CameraView.h"
#include "ZView.h"
#include "Preferences.h"
#include "InspectorControl.h"
#include "Project.h"

id  quakeed_i;
id  entclasses_i;

extern NSBezierPath  *path;
id  g_cmd_out_i;

BOOL    autodirty;
BOOL    filter_light, filter_path, filter_entities;
BOOL    filter_clip_brushes, filter_water_brushes, filter_world;

BOOL  running;

int  bsppid;

#if 0
// example command strings

char  *fullviscmd =
    "rsh satan \"/LocalApps/qbsp $1 $2 ; /LocalApps/light $2 ; /LocalApps/vis $2\"";
char  *fastviscmd =
    "rsh satan \"/LocalApps/qbsp $1 $2 ; /LocalApps/light $2 ; /LocalApps/vis -fast $2\"";
char  *noviscmd =
    "rsh satan \"/LocalApps/qbsp $1 $2 ; /LocalApps/light $2\"";
char    *relightcmd = "rsh satan \"/LocalApps/light $2\"";
char    *leakcmd = "rsh satan \"/LocalApps/qbsp -mark -notjunc $1 $2\"";
#endif

void
NopSound (void)
{
	NSBeep ();
}

void
My_Malloc_Error (int code)
{
// recursive toast  Error ("Malloc error: %i\n", code);
	write (1, "malloc error!\n", strlen ("malloc error!\n") + 1);
}

#define        FN_CMDOUT "/tmp/QuakeEdCmd.txt"
void
DisplayCmdOutput (void)
{
	char    *buffer;
	QFile   *file;
	int     size;

	file = Qopen (FN_CMDOUT, "rt");
	if (!file)
		return;
	size = Qfilesize (file);
	buffer = malloc (size + 1);
	size = Qread (file, buffer, size);
	Qclose (file);
	unlink (FN_CMDOUT);
	[project_i addToOutput: buffer];
	free (buffer);

	if ([preferences_i getShowBSP])
		[inspcontrol_i setCurrentInspector: i_output];

	[preferences_i playBspSound];
}

/*
===============
CheckCmdDone

See if the BSP is done
===============
*/
// DPSTimedEntry cmdte;
void
CheckCmdDone (/* DPSTimedEntry tag, */ double now, void *userData)
{
	union wait      statusp;
	struct rusage   rusage;

	if (!wait4 (bsppid, &statusp, WNOHANG, &rusage))
		return;
	DisplayCmdOutput ();
	bsppid = 0;
//  DPSRemoveTimedEntry( cmdte );
}

void
QuakeEd_print (const char *fmt, va_list args)
{
	static dstring_t    *output;
	NSString            *string;

	if (!output)
		output = dstring_new ();
	dvsprintf (output, fmt, args);
	string = [NSString stringWithCString: output->str];
	[g_cmd_out_i setStringValue: string];
	fputs (output->str, stdout);
}

// ============================================================================

@implementation QuakeEd
/*
===============
AutoSave

Every five minutes, save a modified map
===============
*/
- (void) AutoSave
{
	// automatic backup
	if (autodirty) {
		autodirty = NO;
#define FN_AUTOSAVE @"/qcache/AutoSaveMap.map"
		[map_i writeMapFile: FN_AUTOSAVE useRegion: NO];
	}
	[map_i writeStats];
}

#define FN_TEMPSAVE @"/qcache/temp.map"
- (id) setDefaultFilename
{
	filename = FN_TEMPSAVE;
	[filename retain];
	[self setTitleWithRepresentedFilename: filename];

	return self;
}

- (BOOL) dirty
{
	return dirty;
}

/*
===============================================================================

                DISPLAY UPDATING (handles both camera and XYView)

===============================================================================
*/

BOOL  updateinflight;

BOOL  clearinstance;

BOOL    updatexy;
BOOL    updatez;
BOOL    updatecamera;

void
postappdefined (void)
{
	NSEvent  *ev;

	if (updateinflight)
		return;
	// post an event at the end of the que
	ev = [NSEvent otherEventWithType: NSApplicationDefined
	                        location: NSZeroPoint
	                   modifierFlags: 0
	                       timestamp: [[NSDate date] timeIntervalSinceReferenceDate]
	                    windowNumber: 0
	                         context: [NSApp context]
	                         subtype: 0
	                           data1: 0
	                           data2: 0];
	[NSApp postEvent: ev atStart: NO];
	updateinflight = YES;
}

int  c_updateall;

- (id) updateAll                    // when a model has been changed
{
	updatecamera = updatexy = updatez = YES;
	c_updateall++;
	postappdefined ();

	return self;
}

- (id) updateAll: sender
{
	[self updateAll];

	return self;
}

- (id) updateCamera                 // when the camera has moved
{
	updatecamera = YES;
	clearinstance = YES;

	postappdefined ();

	return self;
}

- (id) updateXY
{
	updatexy = YES;
	postappdefined ();

	return self;
}

- (id) updateZ
{
	updatez = YES;
	postappdefined ();

	return self;
}

- (void) cameraNoRestore: (NSRect)rect
{
	no_restore[0] = YES;
}

- (void) xyNoRestore: (NSRect)rect
{
	no_restore[1] = YES;
}

- (void) zNoRestore: (NSRect)rect
{
	no_restore[2] = YES;
}

- (id) newinstance
{
	clearinstance = YES;
	return self;
}

- (id) redrawInstance
{
	clearinstance = YES;
	[self flushWindow];
	return self;
}

/*
===============
flushWindow

instance draw the brush after each flush
===============
*/
- (void) flushWindow
{
	NSRect  rect;
	int     i;
	NSView  *cv;

	[super flushWindow];

	// don't lock focus before nib is finished loading
	if (!running)
		return;

	if (_disableFlushWindow)
		return;

	cv = [self contentView];
	[cv lockFocus];
	for (i = 3; i >= 0; i--) {
		if (cache[i]) {
			if (!no_restore[i]) {
				rect = cache_rect[i];
				[cache[i] drawAtPoint: rect.origin];
			}
			no_restore[i] = NO;
			[cache[i] release];
			cache[i] = 0;
		}
	}
	rect = [cameraview_i frame];
//	rect = [cv convertRect: rect fromView: cameraview_i];
	cache_rect[0] = rect = NSIntegralRect (rect);
	cache[0] = [[NSBitmapImageRep alloc] initWithFocusedViewRect: rect];

	rect = [[xyview_i superview] frame];
	rect = [cv convertRect: rect fromView: [[xyview_i superview] superview]];
	cache_rect[1] = rect = NSIntegralRect (rect);
	cache[1] = [[NSBitmapImageRep alloc] initWithFocusedViewRect: rect];

	rect = [[zview_i superview] frame];
	rect = [cv convertRect: rect fromView: [[zview_i superview] superview]];
	cache_rect[2] = rect = NSIntegralRect (rect);
	cache[2] = [[NSBitmapImageRep alloc] initWithFocusedViewRect: rect];
	[[self contentView] unlockFocus];

	[cameraview_i lockFocus];
	linestart (0, 0, 0);
	[map_i makeSelectedPerform: @selector (CameraDrawSelf)];
	lineflush ();
	[clipper_i cameraDrawSelf];
	[cameraview_i unlockFocus];

	[xyview_i lockFocus];
	linestart (0, 0, 0);
	[map_i makeSelectedPerform: @selector (XYDrawSelf)];
	lineflush ();
	[cameraview_i XYDrawSelf];
	[zview_i XYDrawSelf];
	[clipper_i XYDrawSelf];
	[xyview_i unlockFocus];

	[zview_i lockFocus];
	[map_i makeSelectedPerform: @selector (ZDrawSelf)];
	[cameraview_i ZDrawSelf];
	[clipper_i ZDrawSelf];
	[zview_i unlockFocus];

	clearinstance = NO;

	[super flushWindow];
}

/*
==============================================================================

App delegate methods

==============================================================================
*/

- (id) applicationDefined: (NSEvent *)theEvent
{
	NSEvent  *evp;

	updateinflight = NO;

	// update screen
	evp = [NSApp   nextEventMatchingMask: NSAnyEventMask
	                           untilDate: [NSDate distantPast]
	                              inMode: NSEventTrackingRunLoopMode
	                             dequeue: NO];
	if (evp) {
		postappdefined ();
		return self;
	}

	[self disableFlushWindow];

	if ([map_i count] != (unsigned) [entitycount_i intValue])
		[entitycount_i setIntValue: [map_i count]];

	if ([[map_i currentEntity] count] != (unsigned) [brushcount_i intValue])
		[brushcount_i setIntValue: [[map_i currentEntity] count]];

	if (updatecamera)
		[cameraview_i display];

	if (updatexy)
		[xyview_i display];

	if (updatez)
		[zview_i display];

	updatecamera = updatexy = updatez = NO;

	[self enableFlushWindow];
	[self flushWindow];

	return self;
}

- (void) awakeFromNib
{
	// XXX [self addToEventMask:
	// XXX NSRightMouseDragged|NSLeftMouseDragged];

	// XXX malloc_error(My_Malloc_Error);

	quakeed_i = self;
	dirty = autodirty = NO;

	[NSTimer   timerWithTimeInterval: 5 * 60
	                          target: self
	                        selector: @selector (AutoSave)
	                        userInfo: nil
	                         repeats: YES];

	path = [NSBezierPath new];
}

- (void) applicationDidFinishLaunching: (NSNotification *)notification
{
	NSArray     *screens;
	NSScreen    *scrn;

	running = YES;
	g_cmd_out_i = cmd_out_i;        // for qprintf
	Sys_SetStdPrintf (QuakeEd_print);

	[preferences_i readDefaults];
	[project_i initProject];

	[xyview_i setModeRadio: xy_drawmode_i];
	// because xy view is inside
	// scrollview and can't be
	// connected directly in IB

	[self setFrameAutosaveName: @"EditorWinFrame"];
	[self clear: self];

	// go to my second monitor
	screens = [NSScreen screens];
	if ([screens count] == 2) {
		NSRect  frm;
		scrn = [screens objectAtIndex: 1];
		frm = [scrn frame];
		[self setFrameTopLeftPoint: NSMakePoint (frm.origin.x, frm.size.height)];
	}

	[self makeKeyAndOrderFront: self];

	[map_i newMap];

	Sys_Printf ("ready.\n");
}

- (id) appWillTerminate: sender
{
	// FIXME: save dialog if dirty
	return self;
}

// ===========================================================================

- (id) textCommand: sender
{
	NSString    *t;

	t = [sender stringValue];

	if (![t isEqualToString: @"texname"]) {
		texturedef_t    *td;
		id              b;

		b = [map_i selectedBrush];
		if (!b) {
			Sys_Printf ("nothing selected\n");
			return self;
		}
		td = [b texturedef];
		Sys_Printf ("%s\n", td->texture);
		return self;
	} else {
		Sys_Printf ("Unknown command\n");
	} return self;
}

- (id) openProject: sender
{
	[project_i openProject];
	return self;
}

- (id) clear: sender
{
	[map_i newMap];

	[self updateAll];
	[regionbutton_i setIntValue: 0];
	[self setDefaultFilename];

	return self;
}

- (id) centerCamera: sender
{
	NSRect  sbounds;

	sbounds = [[xyview_i superview] bounds];

	sbounds.origin.x += sbounds.size.width / 2;
	sbounds.origin.y += sbounds.size.height / 2;

	[cameraview_i setXYOrigin: &sbounds.origin];
	[self updateAll];

	return self;
}

- (id) centerZChecker: sender
{
	NSRect  sbounds;

	sbounds = [[xyview_i superview] bounds];

	sbounds.origin.x += sbounds.size.width / 2;
	sbounds.origin.y += sbounds.size.height / 2;

	[zview_i setPoint: &sbounds.origin];
	[self updateAll];

	return self;
}

- (id) changeXYLookUp: sender
{
	if ([sender intValue])
		xy_viewnormal[2] = 1;
	else
		xy_viewnormal[2] = -1;
	[self updateAll];
	return self;
}

/*
==============================================================================

REGION MODIFICATION

==============================================================================
*/

/*
==================
applyRegion:
==================
*/
- (id) applyRegion: sender
{
	filter_clip_brushes = [filter_clip_i intValue];
	filter_water_brushes = [filter_water_i intValue];
	filter_light = [filter_light_i intValue];
	filter_path = [filter_path_i intValue];
	filter_entities = [filter_entities_i intValue];
	filter_world = [filter_world_i intValue];

	if (![regionbutton_i intValue]) {
		region_min[0] = region_min[1] = region_min[2] = -9999;
		region_max[0] = region_max[1] = region_max[2] = 9999;
	}

	[map_i makeGlobalPerform: @selector (newRegion)];

	[self updateAll];

	return self;
}

- (id) setBrushRegion: sender
{
	id  b;

	// get the bounds of the current selection
	if ([map_i numSelected] != 1) {
		Sys_Printf ("must have a single brush selected\n");
		return self;
	}

	b = [map_i selectedBrush];
	[b getMins: region_min maxs: region_max];
	[b remove];

	// turn region on
	[regionbutton_i setIntValue: 1];
	[self applyRegion: self];

	return self;
}

- (id) setXYRegion: sender
{
	NSRect  bounds;

	// get xy size
	bounds = [[xyview_i superview] bounds];

	region_min[0] = bounds.origin.x;
	region_min[1] = bounds.origin.y;
	region_min[2] = -99999;
	region_max[0] = bounds.origin.x + bounds.size.width;
	region_max[1] = bounds.origin.y + bounds.size.height;
	region_max[2] = 99999;

	// turn region on
	[regionbutton_i setIntValue: 1];
	[self applyRegion: self];

	return self;
}

// UI querie for other objects
- (BOOL) showCoordinates
{
	return [show_coordinates_i intValue];
}

- (BOOL) showNames
{
	return [show_names_i intValue];
}

/*
==============================================================================

BSP PROCESSING

==============================================================================
*/
void
ExpandCommand (const char *in, char *out, const char *src, const char *dest)
{
	while (*in) {
		if (in[0] == '$') {
			if (in[1] == '1') {
				strcpy (out, src);
				out += strlen (src);
			} else if (in[1] == '2') {
				strcpy (out, dest);
				out += strlen (dest);
			}
			in += 2;
			continue;
		}
		*out++ = *in++;
	}
	*out = 0;
}

/*
=============
saveBSP
=============
*/
- (id) saveBSP: (const char *)cmdline dialog: (BOOL)wt
{
	char        expandedcmd[1024];
	NSString    *mappath;
	NSString    *bsppath;
	int         oldLightFilter;
	int         oldPathFilter;
	NSString    *destdir;

	if (bsppid) {
		NSBeep ();
		return self;
	}

	// turn off the filters so all entities get saved
	oldLightFilter = [filter_light_i intValue];
	oldPathFilter = [filter_path_i intValue];
	[filter_light_i setIntValue: 0];
	[filter_path_i setIntValue: 0];
	[self applyRegion: self];

	if ([regionbutton_i intValue]) {
		mappath = [[filename stringByDeletingPathExtension]
					stringByAppendingPathExtension: @"reg"];
		[map_i writeMapFile: mappath useRegion: YES];
		wt = YES;   // allways pop the dialog on region ops
	} else {
		mappath = filename;
	}
	// save the entire thing, just in case there is a problem
	[self save: self];

	[filter_light_i setIntValue: oldLightFilter];
	[filter_path_i setIntValue: oldPathFilter];
	[self applyRegion: self];

	// write the command to the bsp host
	destdir = [project_i getFinalMapDirectory];

	bsppath = [destdir stringByAppendingPathComponent:
				[[[mappath lastPathComponent] stringByDeletingPathExtension]
					stringByAppendingPathExtension: @"bsp"]];

	ExpandCommand (cmdline, expandedcmd,
				   [mappath fileSystemRepresentation],
				   [bsppath fileSystemRepresentation]);

	strcat (expandedcmd, " > ");
	strcat (expandedcmd, FN_CMDOUT);
	strcat (expandedcmd, "\n");
	printf ("system: %s", expandedcmd);

	[project_i addToOutput: (char *) "\n\n========= BUSY =========\n\n"];
	[project_i addToOutput: expandedcmd];

	if ([preferences_i getShowBSP])
		[inspcontrol_i setCurrentInspector: i_output];
	if (wt) {
		id  panel;
		NSModalSession  session;

		panel = NSGetAlertPanel (@"BSP In Progress",
		                         [NSString stringWithCString: expandedcmd],
		                         NULL, NULL, NULL);
		session = [NSApp beginModalSessionForWindow: panel];
		system (expandedcmd);
		[NSApp endModalSession: session];
		[panel close];
		NSReleaseAlertPanel (panel);
		DisplayCmdOutput ();
	} else {
//		cmdte = DPSAddTimedEntry(1, CheckCmdDone, self, NS_BASETHRESHOLD);
		if (!(bsppid = fork ())) {
			system (expandedcmd);
			exit (0);
		}
	}

	return self;
}

- (id) BSP_Full: sender
{
	[self saveBSP: [project_i getFullVisCmd] dialog: NO];
	return self;
}

- (id) BSP_FastVis: sender
{
	[self saveBSP: [project_i getFastVisCmd] dialog: NO];
	return self;
}

- (id) BSP_NoVis: sender
{
	[self saveBSP: [project_i getNoVisCmd] dialog: NO];
	return self;
}

- (id) BSP_relight: sender
{
	[self saveBSP: [project_i getRelightCmd] dialog: NO];
	return self;
}

- (id) BSP_entities: sender
{
	[self saveBSP: [project_i getEntitiesCmd] dialog: NO];
	return self;
}

- (id) BSP_stop: sender
{
	if (!bsppid) {
		NSBeep ();
		return self;
	}

	kill (bsppid, 9);
//	CheckCmdDone (cmdte, 0, NULL);
	[project_i addToOutput: (char *) "\n\n========= STOPPED =========\n\n"];

	return self;
}

/*
==============
doOpen:

Called by open or the project panel
==============
*/
- (id) doOpen: (NSString *)fname;
{
	[fname retain];
	[filename release];
	filename = fname;

	[map_i readMapFile: filename];

	[regionbutton_i setIntValue: 0];
	[self setTitleWithRepresentedFilename: fname];
	[self updateAll];

	return self;
}

/*
==============
open
==============
*/
- (id) open: sender;
{
	id          openpanel;
	NSString    *suffixlist[] = {@"map"};
	NSArray     *types = [NSArray arrayWithObjects: suffixlist count: 1];

	openpanel = [NSOpenPanel new];

	if ([openpanel
	        runModalForDirectory: [project_i getMapDirectory]
	                        file: @""
	                       types: types] != NSOKButton)
		return self;
	[self doOpen: [openpanel filename]];

	return self;
}

/*
==============
save:
==============
*/
- (id) save: sender;
{
	NSString    *backup;
	NSFileManager *fm = [NSFileManager defaultManager];

	// force a name change if using tempname
	if (![filename compare: FN_TEMPSAVE])
		return [self saveAs: self];
	dirty = autodirty = NO;

	backup = [[filename stringByDeletingPathExtension]
				stringByAppendingPathExtension: @"bak"];
	[fm copyPath: filename toPath: backup handler: nil];	// copy old to .bak

	[map_i writeMapFile: filename useRegion: NO];

	return self;
}

/*
==============
saveAs
==============
*/
- (id) saveAs: sender;
{
	id      panel_i;
	NSString    *dir;
	NSString    *fname;

	panel_i = [NSSavePanel new];
	dir = [filename stringByDeletingLastPathComponent];
	[panel_i setRequiredFileType: @"map"];
	if ([panel_i runModalForDirectory: [project_i getMapDirectory] file: dir]
	    != NSOKButton)
		return self;
	fname = [[panel_i filename] retain];
	[filename release];
	filename = fname;

	[self setTitleWithRepresentedFilename: filename];

	[self save: self];

	return self;
}

/*
===============================================================================

                        OTHER METHODS

===============================================================================
*/

//  AJR - added this for Project info
- (NSString *) currentFilename
{
	return filename;
}

- (id) deselect: sender
{
	if ([clipper_i hide])           // first click hides only the clipper
		return [self updateAll];

	[map_i setCurrentEntity: [map_i objectAtIndex: 0]];

	// make world selected
	[map_i makeSelectedPerform: @selector (deselect)];
	[self updateAll];

	return self;
}

/*
===============
keyDown
===============
*/

- (id) keyDown: (NSEvent *)theEvent
{
	NSString    *chars = [theEvent characters];
	unichar     c = ([chars length] == 1) ? [chars characterAtIndex: 0] : '\0';

	// function keys
	switch (c) {
		case NSF2FunctionKey:
			[cameraview_i setDrawMode: dr_wire];
			Sys_Printf ("wire draw mode\n");
			return self;

		case NSF3FunctionKey:
			[cameraview_i setDrawMode: dr_flat];
			Sys_Printf ("flat draw mode\n");
			return self;

		case NSF4FunctionKey:
			[cameraview_i setDrawMode: dr_texture];
			Sys_Printf ("texture draw mode\n");
			return self;

		case NSF5FunctionKey:
			[xyview_i setDrawMode: dr_wire];
			Sys_Printf ("wire draw mode\n");
			return self;

		case NSF6FunctionKey:
			Sys_Printf ("texture draw mode\n");
			return self;

		case NSF8FunctionKey:
			[cameraview_i homeView: self];
			return self;

		case NSF12FunctionKey:
			[map_i subtractSelection: self];
			return self;

		case NSPageUpFunctionKey:
			[cameraview_i upFloor: self];
			return self;

		case NSPageDownFunctionKey:
			[cameraview_i downFloor: self];
			return self;

		case NSEndFunctionKey:
			[self deselect: self];
			return self;

		case NSRightArrowFunctionKey:
		case NSLeftArrowFunctionKey:
		case NSUpArrowFunctionKey:
		case NSDownArrowFunctionKey:
		case 'a':
		case 'A':
		case 'z':
		case 'Z':
		case 'd':
		case 'D':
		case 'c':
		case 'C':
		case '.':
		case ',':
			[cameraview_i _keyDown: theEvent];
			break;

		case 27:    // escape
			autodirty = dirty = YES;
			[self deselect: self];
			return self;

		case 127:   // delete
			autodirty = dirty = YES;
			[map_i makeSelectedPerform: @selector (remove)];
			[clipper_i hide];
			[self updateAll];
			break;

		case '/':
			[clipper_i flipNormal];
			[self updateAll];
			break;

		case 13:    // enter
			[clipper_i carve];
			[self updateAll];
			Sys_Printf ("carved brush\n");
			break;

		case ' ':
			[map_i cloneSelection: self];
			break;

		// move selection keys
		case '2':
			VectorCopy (vec3_origin, sb_translate);
			sb_translate[1] = -[xyview_i gridsize];
			[map_i makeSelectedPerform: @selector (translate)];
			[self updateAll];
			break;

		case '8':
			VectorCopy (vec3_origin, sb_translate);
			sb_translate[1] = [xyview_i gridsize];
			[map_i makeSelectedPerform: @selector (translate)];
			[self updateAll];
			break;

		case '4':
			VectorCopy (vec3_origin, sb_translate);
			sb_translate[0] = -[xyview_i gridsize];
			[map_i makeSelectedPerform: @selector (translate)];
			[self updateAll];
			break;

		case '6':
			VectorCopy (vec3_origin, sb_translate);
			sb_translate[0] = [xyview_i gridsize];
			[map_i makeSelectedPerform: @selector (translate)];
			[self updateAll];
			break;

		case '-':
			VectorCopy (vec3_origin, sb_translate);
			sb_translate[2] = -[xyview_i gridsize];
			[map_i makeSelectedPerform: @selector (translate)];
			[self updateAll];
			break;

		case '+':
			VectorCopy (vec3_origin, sb_translate);
			sb_translate[2] = [xyview_i gridsize];
			[map_i makeSelectedPerform: @selector (translate)];
			[self updateAll];
			break;

		default:
			Sys_Printf ("undefined keypress\n");
			NopSound ();
			break;
	} /* switch */

	return self;
}

@end
