text:
#import "MyDocument.h"
#import "StoryTellingEditorAppDelegate.h"
#import <AFCache/AFCacheLib.h>
#import <EngineRoom/CrossPlatform_Utilities.h>
#import <EngineRoom/tracer.h>
#import "ObjectExchange.h"
#import "NSManagedObject_DictionaryRepresentation.h"
#import "Convenience_Macros.h"
#import "FileUtil.h"
#import "NSWindow+Util.h"
#import "MediaContainer.h"
#import "NSString+md5.h"

#define MODEL_KEYPATH(base, rest) GENERIC_MODEL_KEYPATH(nil, (base), (rest))
#define MODEL_ACCESS(base, rest)  [self valueForKeyPath: MODEL_KEYPATH((base), (rest))]

#define TOGGLE_OBSERVERS 0

#if ! TOGGLE_OBSERVERS
#endif

static NSString *kDocumentTypePSD = @"PSD";
static NSString *kDocumentTypeTIFF = @"TIFF";
static NSString *kDocumentTypeDictionary = @"Dictionary";
static NSString *kDocumentTypeSTPNGImport = @"STPNGImport";
static NSString *kAFPSDImportBundleName	= @"AFPSDImport";
static NSString *kAFPSDImportBundleExtension = @"app";
static NSString *kAFPSDImportBundleCLIToolXee = @"afpsdcli";
static NSString *kAFPSDImportBundleCLIToolImageMagick = @"afmagickcli";
static NSString *kAFPSDImportPrototypeDocumentName = @"DefaultPrototypeDocument";
static NSString *kAFPSDImportPrototypeSceneName = @"DefaultPrototypeScene";
static NSString *kStoryTellingPrototypeAssetName = @"DefaultPrototypeAsset";
static NSString *kStoryTellingPrototypeEmptyDocumentName = @"DefaultEmptyStoryTellingPrototypeDocument";

@implementation MyDocument

@synthesize stageArrayController;
@synthesize scenarioController;
@synthesize playableArrayController;
@synthesize sceneArrayController;
@synthesize assetArrayController;
@synthesize keyframeAnimationController;
@synthesize keyframeArrayController;
@synthesize unionOfSceneKeyframesArrayController;
@synthesize editorWindow;
@synthesize statusDisplay, statusDisplayToolbarItem;
@synthesize physicsSwitchToolbarItem, physicsSwitch;
@synthesize progressSheet;
@synthesize progressTitleField;
@synthesize progressIndicator;
@synthesize stageEditorViewController;
@synthesize stageEditorViewPlaceholder;
@synthesize importTaskWrapper;
@synthesize disableObserversCount;
@synthesize previewController;

@synthesize psdImportScale = _psdImportScale;
@synthesize editorWindowDragTypes = _editorWindowDragTypes;

#pragma mark -
#pragma mark Candidates for refactoring

// located here because it is addressed via representedObject from CanvasSceneView
// could move into a dedicated model manager

// Is the created object autoreleased?
// If so, the method must be renamed
// BK: I don't think so - this is not CF Level - Cocoa rules are: alloc new (mutable)[cC]opy
// It should also return the created object

// used by drag code and by the MergeDocumentsWizardController
- (void)createAssetFromDragData:(AssetDragVO *)dragData atPoint:(CGPoint)aPoint {
	NSError *error = nil;
	AssetMO *asset = [[self objectsFromPlistResourceNamed: kStoryTellingPrototypeAssetName error: &error] lastObject];

	if( nil == asset ) {
		NSLog(@"could not create prototype asset: %@", error);
		[self presentError: error];
	} else {
		asset.name = dragData.name;

		switch (dragData.type) {
			case AssetDragVOTypeImage:
				asset.kind = [NSNumber numberWithInteger:AssetMOKindImage];
				break;
			case AssetDragVOTypeVideo:
				asset.kind = [NSNumber numberWithInteger:AssetMOKindVideo];
				break;
		}

		NSDictionary *externalDataProperties = ER_DICT(
													   @"keyName", kSTEBlobRefKeyPrimary,
													   @"externalURL", (id)dragData.externalURL ?: (id)[NSNull null],
   													   @"externalId", (id)dragData.externalId   ?: (id)[NSNull null],
   													   @"contentType", (id)dragData.contentType ?: (id)[NSNull null],
													   );

		asset.primaryBlob = [self externalDataWithProperties: externalDataProperties];

		asset.viewPosition = [NSNumber numberWithInteger: [MODEL_ACCESS(kModelAccessAssetsKeyPath, nil) count]];
		[[asset.keyframeAnimation.keyframes anyObject] setValue:[NSValue valueWithCGPoint:aPoint] forKey:@"position"];

		[MODEL_ACCESS(kModelAccessAssetArrayControllerKeyPath, nil) addObject: asset];
	}
	NSLog(@"assetArrayController: %@", self.assetArrayController);
}

// putting it here because it should placed elsewhere after refactoring
- (void)createAssetContainingChildren:(NSArray *)children withName:(NSString*)name {
	NSError *error = nil;
	AssetMO *parentAsset = [[self objectsFromPlistResourceNamed: kStoryTellingPrototypeAssetName error: &error] lastObject];
	if( nil == parentAsset ) {
		NSLog(@"could not create prototype asset: %@", error);
		[self presentError: error];
	} else {
		parentAsset.name = name;
		parentAsset.viewPosition = [NSNumber numberWithInteger: [MODEL_ACCESS(kModelAccessAssetsKeyPath, nil) count]];
		[[parentAsset.keyframeAnimation.keyframes anyObject] setValue:[NSValue valueWithCGPoint:CGPointZero] forKey:@"position"];
		[MODEL_ACCESS(kModelAccessAssetArrayControllerKeyPath, nil) addObject: parentAsset];
		for (AssetMO *child in children) {
			child.parent = parentAsset;
		}
	}
}

- (ExternalDataMO *) externalDataWithProperties: (NSDictionary *) properties;
{
	NSError *error = nil;

	NSMutableDictionary *mutableProperties = [NSMutableDictionary dictionaryWithDictionary: properties];

	[mutableProperties setObject: @"ExternalData" forKey: kObjectExchangeDictionaryRepresentationEntityNameKey];

	ExternalDataMO *externalDataMO = [ExternalDataMO objectFromDictionaryRepresentation: mutableProperties inManagedObjectContext: self.managedObjectContext options: nil error: &error];

	if( nil == externalDataMO ) {
		lpdebug(error, error.userInfo);
		[self presentError: error];
	}

	lpdebug(externalDataMO);

	return externalDataMO;
}

- (BOOL)itemsMayBeGrouped:(NSArray *)items {
	for (AssetMO *asset in items) {
		if (asset.parent != nil) return NO;
	}
	return YES;
}

- (void) makeCurrentInAppDelegate: (BOOL) active
{
	[[[NSApp delegate] valueForKey: @"assetEditorController"] setValue: active ? self.stageEditorViewController.assetEditor : nil forKey: @"content"];
}

// for debugging
- (void) objectsDidChange: (NSNotification *) n
{
	[[n.userInfo objectForKey: NSUpdatedObjectsKey] enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
	}];
}

#pragma mark -
#pragma mark Construction

- (void)awakeFromNib
{
	[statusDisplayToolbarItem setView:statusDisplay];
	[statusDisplayToolbarItem setMinSize:NSMakeSize(415, 40)];
	[physicsSwitchToolbarItem setView:physicsSwitch];
	[physicsSwitchToolbarItem setMinSize:NSMakeSize(96, 21)];
}

- (NSString *)windowNibName
{
    return [[NSApp currentEvent] modifierFlags] & NSControlKeyMask ? @"DebugDocument" : @"StoryTellingDocument";
}

- (void)windowControllerDidLoadNib:(NSWindowController *)windowController
{
    [super windowControllerDidLoadNib:windowController];

	// for re-import dragging of stpngimport files
	self.editorWindowDragTypes = [NSArray arrayWithObjects: (id)kUTTypeFileURL, nil];
	// disabled, not yet done [[windowController window] registerForDraggedTypes: self.editorWindowDragTypes];

	NSArray *nameSortDescriptors = [NSArray arrayWithObject: [NSSortDescriptor sortDescriptorWithKey: @"name" ascending: YES]];
	NSArray *sceneViewPositionSortDescriptors = [NSArray arrayWithObject: [NSSortDescriptor sortDescriptorWithKey: @"viewPosition" ascending: YES]];
	NSArray *assetViewPositionSortDescriptors = [NSArray arrayWithObject: [NSSortDescriptor sortDescriptorWithKey: @"viewPosition" ascending: NO]];
	NSArray *timeSortDescriptors = [NSArray arrayWithObject: [NSSortDescriptor sortDescriptorWithKey: @"time" ascending: YES]];

	[self.stageArrayController setSortDescriptors: nameSortDescriptors];
	[self.sceneArrayController setSortDescriptors: sceneViewPositionSortDescriptors];
	[self.assetArrayController setSortDescriptors: assetViewPositionSortDescriptors];
	[self.keyframeArrayController setSortDescriptors: timeSortDescriptors];

	//self.stageEditorViewController = [[StageEditorViewController viewControllerReplacingPlaceholderView: stageEditorViewPlaceholder mainArrayControllerKeyPath: kModelAccessSceneArrayControllerKeyPath] retain];
	self.stageEditorViewController = [[StageEditorViewController viewControllerReplacingPlaceholderView: stageEditorViewPlaceholder] retain];

	[self enableObservers];

	[self performSelector:@selector(postInit) withObject:nil afterDelay:0.0];

	// [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(objectsDidChange:) name: NSManagedObjectContextObjectsDidChangeNotification object: nil];
}

- (void)postInit {
//	[self setDocumentSettingsDate];
}

- (void)setDocumentSettingsDate {
}

- (id)init
{
	if (self = [super init]){
		_playTimer = nil;
		disableObserversCount = 1; // initially disabled

		_psdImportScale = [[NSUserDefaults standardUserDefaults] doubleForKey: @"psdImportScale"];
		if( fabs(_psdImportScale) < 0.01 ) {
			_psdImportScale = 1.0;
		}
		NSLog(@"psdImportScale: %.3lf", _psdImportScale);
	}
	return self;
}

- (id)initWithType:(NSString *)typeName error:(NSError **)outError {
	if (self = [super initWithType:typeName error:outError]) {
		NSError *error = nil;

		NSArray *objectsRead = nil;

		NSManagedObjectContext *moc = [self managedObjectContext];

		[moc processPendingChanges];
		[[moc undoManager] disableUndoRegistration];

		objectsRead = [self objectsFromPlistResourceNamed: kStoryTellingPrototypeEmptyDocumentName error: &error];

		[moc processPendingChanges];
		[[moc undoManager] enableUndoRegistration];

		if( nil == objectsRead ) {
			[NSApp presentError: error];
		}
	}
	return self;
}

- (id)initWithContentsOfURL:(NSURL *)absoluteURL ofType:(NSString *)typeName error:(NSError **)outError
{
	if( YES == [typeName isEqualToString: kDocumentTypePSD] ||
		YES == [typeName isEqualToString: kDocumentTypeTIFF] ||
		YES == [typeName isEqualToString: kDocumentTypeDictionary] ||
		YES == [typeName isEqualToString: kDocumentTypeSTPNGImport]
		) {

		Class myClass = [self class];

		[self release]; // we are re-birthing as a default document and then import the data
		self = [[myClass alloc] init];

		if( ( nil != self ) ) {

			if( NO == [self readFromURL: absoluteURL ofType: typeName error: outError] ) {
				[self release];
				self = nil;
			}
		}

		return self;
	}

	// Display alert if migration will occur
	BOOL isMigrationNecessary = [self isMigrationNecessaryForURL:absoluteURL ofType:typeName];
	if (isMigrationNecessary == YES) {
		NSAlert *alert = [[[NSAlert alloc] init] autorelease];
		[alert addButtonWithTitle:@"Öffnen"];
		[alert addButtonWithTitle:@"Abbruch"];
		[alert setMessageText:@"Aktualisierung des Dokuments notwendig"];
		NSString *backupDocumentName = [NSString stringWithFormat:@"%@~%@",
										[[[absoluteURL path] lastPathComponent] stringByDeletingPathExtension],
										[[absoluteURL path] pathExtension]];

		NSString *txt = [NSString stringWithFormat:@"Sie öffnen ein Dokument, das mit einer älteren Version des StoryTellingEditors erstellt wurde.\n\nWenn sie fortfahren, wird das Dokument aktualisiert und Sie können es danach nicht mehr mit der älteren Version des StoryTellingEditors öffnen.\n\nEine Sicherheitskopie der alten Version wird unter\n\"%@\" abgelegt.", backupDocumentName];
		[alert setInformativeText:txt];
		[alert setAlertStyle:NSWarningAlertStyle];

		if ([alert runModal] == NSAlertFirstButtonReturn) {
			return [super initWithContentsOfURL: absoluteURL ofType: typeName error: outError];
		} else {
			*outError = [NSError errorWithDomain:NSCocoaErrorDomain code:NSUserCancelledError userInfo:nil];
			return nil;
		}
	}
	return [super initWithContentsOfURL: absoluteURL ofType: typeName error: outError];
}

- (BOOL)isMigrationNecessaryForURL:(NSURL*)sourceStoreURL ofType:(NSString*)sourceStoreType {
	NSPersistentStoreCoordinator *psc = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
	NSError *error = nil;
	NSDictionary *sourceMetadata =
    [NSPersistentStoreCoordinator metadataForPersistentStoreOfType:sourceStoreType
															   URL:sourceStoreURL
															 error:&error];
	if (sourceMetadata == nil) {
		// TODO: deal with error
		lpdebug(@"sourceMetadata is nil");
	}

	lpdebug(sourceMetadata);

	NSString *configuration = nil;
	NSManagedObjectModel *destinationModel = [psc managedObjectModel];
	BOOL pscCompatibile = [destinationModel isConfiguration:configuration compatibleWithStoreMetadata:sourceMetadata];

	return (pscCompatibile == YES)?NO:YES;
}

#pragma mark -
#pragma mark Destruction

- (void) close
{
	NSLog(@"close %@", self);
#if TOGGLE_OBSERVERS
	[self disableObservers];
#endif
	[super close];
}

- (void) dealloc
{
	NSLog(@"dealloc %@", self);

	// [[NSNotificationCenter defaultCenter] removeObserver: self];

	[self disableObservers];

	[stageArrayController release];
	[scenarioController release];
	[playableArrayController release];
	[sceneArrayController release];
	[assetArrayController release];
	[keyframeArrayController release];
	[unionOfSceneKeyframesArrayController release];

	[progressTitleField release];
	[progressIndicator release];
	[progressSheet release];

	[statusDisplayToolbarItem release];
	[statusDisplay release];
	[editorWindow release];
	[_editorWindowDragTypes release];

	[previewController release];

	[super dealloc];
}

#pragma mark -
#pragma mark Window delegate methods

- (void) windowDidBecomeMain: (NSNotification *) notification {
	[self makeCurrentInAppDelegate: YES];
}

- (void) windowDidResignMain: (NSNotification *) notification {
	[self makeCurrentInAppDelegate: NO];
}

- (void) windowWillClose: (NSNotification *) notification {
	[self makeCurrentInAppDelegate: NO];
}

#pragma mark -
#pragma mark Window delegate methods (Drag n Drop)

- (NSURL *) _fileURLFromDraggingInfo: (id <NSDraggingInfo>) draggingInfo
{
	NSPasteboard *pboard = [draggingInfo draggingPasteboard];

	NSString *type = [pboard availableTypeFromArray: self.editorWindowDragTypes];
	NSString *draggedURLText = nil;
	if( nil != type ) {
		draggedURLText = [pboard stringForType: type];
	}

	NSLog(@"url: %@ types: %@", draggedURLText, [pboard types]);

	return draggedURLText ? [NSURL URLWithString: draggedURLText] : nil;
}

- (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender{
	return [[[self _fileURLFromDraggingInfo: sender] pathExtension] isEqualToString: @"stpngimport"] ? NSDragOperationLink /* for the link arrow */ : NSDragOperationNone;
}

- (BOOL)prepareForDragOperation:(id <NSDraggingInfo>)sender{
	return YES;
}

- (BOOL)performDragOperation:(id <NSDraggingInfo>)sender{

	NSURL *draggedFileURL = [self _fileURLFromDraggingInfo: sender];
	BOOL success = NO;
	NSError *error = nil;

	if( nil != draggedFileURL ) {
		//success = [self reimportFromURL: draggedFileURL ofType: kDocumentTypeSTPNGImport error: &error];
		if( NO == success ) {
			[self presentError: error];
		}
	} else {
		SET_NSERROR_REASON(&error, NSPOSIXErrorDomain, EINVAL, @"EXPECTED_FILE_URL_GOT_NONE");
		[self presentError: error];
	}

	return success;
}

#pragma mark -
#pragma mark Controlling observers
- (void) enableObservers
{
	NSLog(@"%@", tracerBacktraceAsString(0));
	if( self.disableObserversCount == 1 ) {
		NSLog(@"enabling observers");
		[self.stageEditorViewController bind:@"representedObject" toObject: self withKeyPath: @"self" options: nil];

		self.disableObserversCount = self.disableObserversCount - 1;

	} else {
		NSLog(@"ignoring enableObservers - disableCount: %ld", (long) self.disableObserversCount);
	}
}

- (void) disableObservers
{
	NSLog(@"%@", tracerBacktraceAsString(0));
	[[self class] cancelPreviousPerformRequestsWithTarget: self selector: @selector(enableObservers) object: [NSNull null]];

	if( 0 == self.disableObserversCount ) {
		NSLog(@"disabling observers");
		[self.stageEditorViewController unbind: @"representedObject"];
		[self.stageEditorViewController setRepresentedObject: nil];
		self.disableObserversCount = self.disableObserversCount + 1;
	} else {
		NSLog(@"ignoring disableObservers - disableCount: %ld", (long) self.disableObserversCount);
	}
}

#pragma mark -
#pragma mark Actions and friends

#pragma mark -
#pragma mark Physics stuff

- (IBAction)togglePhysicsMode:(id)sender {
	NSSegmentedControl *seg = sender;

	BOOL canSwitch = [self documentHasKeyframeAnimationsOrMultipleScenes]==NO?YES:NO;
	for (AssetMO *asset in [assetArrayController arrangedObjects]) {
		if ([[asset keyframes] count] > 1) {
			canSwitch = NO;
			break;
		}
	}
	if (canSwitch == NO) {
		[seg setSelectedSegment:SwitchModeOff];
		StageMO *stage = MODEL_ACCESS(kModelAccessStageSelectionKeyPath, @"self");
		if( NSNoSelectionMarker == stage ) return;
		[stage setValue:[NSNumber numberWithBool:NO] forKey:@"physicsEnabled"];
		NSBeginCriticalAlertSheet(@"Umschalten nicht möglich.",
									@"OK",
									nil, nil, [NSApp keyWindow], self,
									@selector(sheetDidEnd:returnCode:contextInfo:),
									@selector(sheetDidDismiss:returnCode:contextInfo:), nil,
									@"Um in den Physik-Modus zu wechseln, darf das Dokument nur eine Szene enthalten. Weiterhin dürfen keine Animationen (mehr als ein Keyframe pro Asset) vorhanden sein.");
		return;
	}

	StageMO *stage = MODEL_ACCESS(kModelAccessStageSelectionKeyPath, @"self");
	BOOL physicsEnabled = !NSIsControllerMarker(stage) && [[stage valueForKey:@"physicsEnabled"] boolValue];

	[[[self.stageEditorViewController.timelineViewController timeline] gridView] setEnabled:!physicsEnabled];
//	[self.stageEditorViewController.timelineViewController.splitView setPosition:pos ofDividerAtIndex:0];
}

- (BOOL)documentHasKeyframeAnimationsOrMultipleScenes {
	BOOL multiple = ([[self.sceneArrayController arrangedObjects] count] != 1);
	for (AssetMO *asset in [assetArrayController arrangedObjects]) {
		if ([[asset keyframes] count] > 1) {
			multiple = YES;
			break;
		}
	}
	return multiple;
}

- (void)sheetDidEnd:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(void  *)contextInfo {
}

- (void)sheetDidDismiss:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(void  *)contextInfo {
}

- (NSInteger)lastFrame {
  NSInteger lastFrame = 0;
	NSInteger t = 0;
  for (AssetMO *asset in [assetArrayController arrangedObjects]) {
    if ([[asset hidden] boolValue]) continue;
	  t = [asset range].length + [asset range].location;
    lastFrame = fmax(lastFrame, t);
  }
  return lastFrame;
}

- (IBAction)rewind:(id)sender{
	NSNumber *theTime = [NSNumber numberWithInteger: 0];
	[self.stageEditorViewController.sceneViewController setCurrentTime: theTime];
}

- (IBAction)play:(id)sender{
	NSInteger currentTime = [self.stageEditorViewController.sceneViewController.currentTime
		integerValue];
	if ([self lastFrame] - 1 == currentTime){
		self.stageEditorViewController.sceneViewController.currentTime = [NSNumber numberWithInt:0];
	}

	if (_playTimer){
		[_playTimer invalidate];
		_playTimer = nil;
		[self updatePlayButton];
		return;
	}
	_playTimer = [NSTimer scheduledTimerWithTimeInterval:1.0f/25.0f target:self
		selector:@selector(doPlay:) userInfo:[NSNumber numberWithInt:[self lastFrame]] repeats:YES];
	[self updatePlayButton];
}

- (void)doPlay:(NSTimer *)aTimer{
	NSInteger theLastFrame = [[aTimer userInfo] integerValue];
	NSInteger currentTime = [self.stageEditorViewController.sceneViewController.currentTime
		integerValue];
	if (currentTime + 1 >= theLastFrame){
		[_playTimer invalidate];
		_playTimer = nil;
		[self updatePlayButton];
		return;
	}
	NSNumber *theTime = [NSNumber numberWithInteger: currentTime + 1];
	[self.stageEditorViewController.sceneViewController setCurrentTime: theTime];
//	self.assetEditor.currentTime = theTime;
}

- (void)updatePlayButton{
	if (_playTimer){
		[playButton setImage:[NSImage imageNamed:@"maintoolbar-icon-pause.png"]];
	}else{
		[playButton setImage:[NSImage imageNamed:@"maintoolbar-icon-play.png"]];
	}
}

- (IBAction) test: (id) sender
{
	NSLog(@"sac: %@", [self.sceneArrayController sortDescriptors]);
}

- (IBAction) producePageLayoutData: (id) sender
{
	BOOL normalizeCoordinates = YES;
	StageMO *stage = MODEL_ACCESS(kModelAccessStageSelectionKeyPath, @"self");
	CGSize pageSize = CGSizeZero;
	NSString *tmpDir = NSTemporaryDirectory();

	if (stage) {
		pageSize = CGSizeMake([[stage valueForKey: @"pageWidth"] floatValue],
							   [[stage valueForKey: @"pageHeight"] floatValue]);

		if( pageSize.width <= 0 || pageSize.height <= 0) {
			pageSize = CGSizeMake([[stage valueForKey: @"width"] floatValue],
								[[stage valueForKey: @"height"] floatValue]);
		}
	}

	if( pageSize.width <= 0 || pageSize.height <= 0) {
		pageSize = CGSizeMake(1, 1); // to avoid div by zero, equal to no scaling
	}

	NSString *basePath = [[NSBundle mainBundle] pathForResource:@"main" ofType:@"css"];
	basePath = [basePath stringByDeletingLastPathComponent];
	//NSString *logoPath = [[NSBundle mainBundle] pathForResource:@"aflogo" ofType:@"png"];
	NSMutableString *HTML = [NSMutableString stringWithFormat:
							 @"<%%@page contentType=\"text/html\" pageEncoding=\"UTF-8\"%%>" \
							 "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\"" \
							 "\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">" \
							 "<html xmlns=\"http://www.w3.org/1999/xhtml\" lang=\"en\" xml:lang=\"en\">" \
							 "<head>" \
							 "<title>StoryTelling Editor</title>" \
							 "<link href=\"%@/main.css\" type=\"text/css\" rel=\"stylesheet\" />" \
							 "</head>" \
							 "<body\">" \
							 "<div id=\"container\">" \
							 "<h1>Layout data & export statistics</h1>", basePath, basePath, basePath, basePath, basePath];

	[HTML appendString: @"<form><table>"];

	[HTML appendFormat: @"<tr><th>%@</th><th>%@</th><th>%@</th><th>%@</th><th>%@</th><th>%@</th><th>%@</th><th>%@</th></tr>",
				@"zPosition",
				@"Name",
				@"xOffset",
				@"yOffset",
				@"width",
				@"height",
				@"size k",
				@"LayerImage"
		];

	unsigned long long totalImageFileSize = 0;
	NSMutableString *chartValues = [NSMutableString stringWithString:@"["];
	NSMutableString *legendValues = [NSMutableString stringWithString:@"["];

	NSArray *assets = MODEL_ACCESS(kModelAccessAssetsKeyPath, nil);
	int assetCounter = 0;

	for( AssetMO *asset in assets ) {
		NSArray *orderedKeyframes = [asset valueForKeyPath: @"keyframeAnimation.orderedKeyframes"];

		if( [orderedKeyframes count] < 1 ) {
			NSLog(@"%@ WARNING !!!! - accessing layer (%@) without keyframes", [self class], asset);
			return;
		}

		KeyframeMO *kf = [orderedKeyframes objectAtIndex: 0];

		CGPoint position = [[kf valueForKey: @"position"] CGPointValue];
		CGRect bounds = [[kf valueForKey: @"bounds"] CGRectValue];

		if (normalizeCoordinates) {
			position.x /= pageSize.width;
			position.y /= pageSize.height;
			bounds.size.width /= pageSize.width;
			bounds.size.height /= pageSize.height;
		}

		NSString *imageURLString = nil;

		[asset.primaryBlob.mediaContainer updateRenderings];
		unsigned long long fileSize = [asset.primaryBlob.mediaContainer.renderedData length];
		imageURLString = [asset.primaryBlob.mediaContainer.renderedURL absoluteString];

		totalImageFileSize += fileSize;

		[HTML appendFormat: @"<tr><td><input size=\"2\" type=\"text\" value=\"%@\"></td>" \
		 "<td><input size=\"25\" type=\"text\" value=\"%@\"></td>" \
		 "<td><input size=\"8\" type=\"text\" value=\"%.4f\"></td>" \
		 "<td><input size=\"8\" type=\"text\" value=\"%.4f\"></td>" \
		 "<td><input size=\"8\" type=\"text\" value=\"%.4f\"></td>" \
		 "<td><input size=\"8\" type=\"text\" value=\"%.4f\"></td>" \
		 "<td><input size=\"8\" type=\"text\" value=\"%.4f\"></td>" \
		 "<td>" \
		 "<a href=\"%@\"><img src=\"%@\" width=\"128\"/></a></td></tr>",
				asset.viewPosition,
				asset.name,
				position.x, position.y,
				CGRectGetWidth(bounds), CGRectGetHeight(bounds),
				(float)fileSize / 1000,
				imageURLString,
				imageURLString
		];
		[chartValues appendFormat:@"%.2f", (float)fileSize / 1000];
		[legendValues appendFormat:@"\"%%%%.%%%% - %@\"", asset.name];
		if ([assets count]-1 > assetCounter) {
			[chartValues appendString:@", "];
			[legendValues appendString:@", "];
		}
		assetCounter++;
		//[mediaContainer release];

	}

	[HTML appendFormat: @"<tr><td><strong>Total file size (images): %f</strong></td></tr>", (float)totalImageFileSize / 1000];
	[HTML appendString: @"</table></form>"];
	[HTML appendString: @"</div>"];
	[HTML appendString: @"<div id=\"piechart\"></div>"];

	[chartValues appendString:@"]"];
	[legendValues appendString:@"]"];

	[HTML appendString: @"<script type=\"text/javascript\">"];
	[HTML appendString: @"var pie = null;"];
	[HTML appendString: @"function chart() {"];
	[HTML appendString: @"var r = Raphael(\"piechart\", 640, 200);"];
	[HTML appendFormat: @"pie = r.g.piechart(100, 100, 100, %@, {legend: %@, legendpos: 'east'});", chartValues, legendValues];
	[HTML appendString: @"}"];
	[HTML appendString: @"</script>"];

	//[HTML appendFormat:@"<br/><img src=\"%@\" alt=\"Artifacts\">", logoPath];
	[HTML appendString: @"<br/></body></html>"];

	NSString *HTMLFile = [tmpDir stringByAppendingFormat: @"PageLayout-%.1lf.html", [NSDate timeIntervalSinceReferenceDate]];

	NSError *error = nil;

	if( NO == [HTML writeToFile: HTMLFile atomically: YES encoding: NSUTF8StringEncoding error: &error] ) {
		[NSApp presentError: error];
	} else {
		[[NSWorkspace sharedWorkspace] openFile: HTMLFile];
	}
}

#pragma mark -
#pragma mark CoreData versioning

- (BOOL)configurePersistentStoreCoordinatorForURL:(NSURL*)url
										   ofType:(NSString*)fileType
							   modelConfiguration:(NSString*)configuration
									 storeOptions:(NSDictionary*)storeOptions
											error:(NSError**)error {
	NSMutableDictionary *options = nil;
	if (storeOptions != nil) {
		options = [storeOptions mutableCopy];
	} else {
		options = [[NSMutableDictionary alloc] init];
	}

	[options setObject:[NSNumber numberWithBool:YES]
				forKey:NSMigratePersistentStoresAutomaticallyOption];
//	[options setObject:[NSNumber numberWithBool:YES]
//				forKey:NSInferMappingModelAutomaticallyOption];

	BOOL result = [super configurePersistentStoreCoordinatorForURL:url
															ofType:fileType
												modelConfiguration:configuration
													  storeOptions:options
															 error:error];
	[options release], options = nil;
	return result;
}

- (void)document:(NSDocument *)doc didSaveForBackwardCompatibleCopy:(BOOL)didSave contextInfo:(void  *)contextInfo
{
	lpkdebug("backwardCompatibleMigration", didSave);

	if( NO == didSave ) {
		return;
	}

	NSError *error = nil;

	NSBundle *classBundle = [NSBundle bundleForClass: [self class]];

	NSArray *modelBundles = [NSArray arrayWithObject: classBundle];

	NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];

	NSString *backwardCompatibleModel = [defaults valueForKey: kSTEBackwardCompatibleModel];
	NSString *backwardCompatibleVersionSuffix = [defaults valueForKey: kSTEBackwardCompatibleVersionSuffix];
	NSString *backwardCompatibleFileType = [backwardCompatibleVersionSuffix pathExtension];

	if( NO == CHECK_NSERROR_REASON(nil != backwardCompatibleModel && nil != backwardCompatibleVersionSuffix,
		&error, NSPOSIXErrorDomain, ENOENT, @"errorMissingConfigurationForBackwardCompatibleSave") ) {
		lpkdebug("backwardCompatibleMigration", error);
		[self presentError: error];
		return;
	}

	NSString *sourceType = [self persistentStoreTypeForFileType: [self fileType]];
	NSString *destinationType = [self persistentStoreTypeForFileType: backwardCompatibleFileType];

	lpkdebug("backwardCompatibleMigration", backwardCompatibleModel, backwardCompatibleVersionSuffix, backwardCompatibleFileType, sourceType, destinationType);

	NSString *destinationModelPath = [[classBundle resourcePath] stringByAppendingPathComponent: backwardCompatibleModel];

	NSManagedObjectModel *destinationModel = [[[NSManagedObjectModel alloc] initWithContentsOfURL: [NSURL fileURLWithPath: destinationModelPath]] autorelease];

	if( NO == CHECK_NSERROR_REASON(nil != destinationModel, &error, NSPOSIXErrorDomain, ENOENT, @"errorNoDestinationModelForBackwardCompatibleSave") ) {
		lpkdebug("backwardCompatibleMigration", error);
		[self presentError: error];
		return;
	}

	NSManagedObjectModel *sourceModel = [self managedObjectModel];

	NSSet *sourceVersionIdentifiers = [sourceModel versionIdentifiers];
	NSSet *destinationVersionIdentifiers = [destinationModel versionIdentifiers];

	lpkdebug("backwardCompatibleMigration", sourceVersionIdentifiers, destinationVersionIdentifiers);

	if( NO == CHECK_NSERROR_REASON(1 == [sourceVersionIdentifiers count], &error, NSPOSIXErrorDomain, ENOENT, @"errorSourceModelForBackwardCompatibleSaveHasNotExactlyOneVersionIdentifier") ) {
		lpkdebug("backwardCompatibleMigration", error);
		[self presentError: error];
		return;
	}

	if( NO == CHECK_NSERROR_REASON(1 == [destinationVersionIdentifiers count], &error, NSPOSIXErrorDomain, ENOENT, @"errorSourceModelForBackwardCompatibleSaveHasNotExactlyOneVersionIdentifier") ) {
		lpkdebug("backwardCompatibleMigration", error);
		[self presentError: error];
		return;
	}

	NSMigrationManager *migrationManager = [[[NSMigrationManager alloc] initWithSourceModel: sourceModel destinationModel: destinationModel] autorelease];

	NSMappingModel *mappingModel = [NSMappingModel mappingModelFromBundles: modelBundles forSourceModel: sourceModel destinationModel: destinationModel];

	if( NO == CHECK_NSERROR_REASON(nil != mappingModel, &error, NSPOSIXErrorDomain, ENOENT, @"errorNoMappingModelForBackwardCompatibleSave") ) {
		lpkdebug("backwardCompatibleMigration", error);
		[self presentError: error];
		return;
	}

	NSURL *currentURL = [self fileURL];

	lpkdebug("backwardCompatibleMigration", currentURL);

	NSString *path = [currentURL path];

	NSURL *destinationURL = [NSURL fileURLWithPath: [[path stringByDeletingPathExtension] stringByAppendingString: backwardCompatibleVersionSuffix]];

	lpkdebug("backwardCompatibleMigration", destinationURL);

	if( NO == [migrationManager migrateStoreFromURL: currentURL type: sourceType options: nil withMappingModel: mappingModel toDestinationURL: destinationURL destinationType: destinationType destinationOptions: nil error: &error] ) {
		lpkdebug("backwardCompatibleMigration", error);
	}
}

- (IBAction) saveDocumentAndCreateBackwardCompatibleCopy: (id) sender
{
	[self saveDocumentWithDelegate: self didSaveSelector: @selector(document:didSaveForBackwardCompatibleCopy:contextInfo:) contextInfo: NULL];
}

#pragma mark -
#pragma mark Undo utilities

- (BOOL) enableUndo: (BOOL) shouldEnableUndo
{
	NSManagedObjectContext *moc = [self managedObjectContext];
	NSUndoManager *undoManager = [moc undoManager];
	BOOL wasUndoRegistrationEnabled = [undoManager isUndoRegistrationEnabled];

	if ( wasUndoRegistrationEnabled != shouldEnableUndo ) {
		[moc processPendingChanges];
		[[moc undoManager] performSelector: shouldEnableUndo ? @selector(enableUndoRegistration) : @selector(disableUndoRegistration)];
	}

	return wasUndoRegistrationEnabled;
}

#pragma mark -
#pragma mark Housekeeping

#define kSTEConsistencyCheckOptionConfigured	0x01
#define kSTEConsistencyCheckOptionRun			0x02
#define kSTEConsistencyCheckOptionShow			0x04
#define kSTEConsistencyCheckOptionAsk			0x08
#define kSTEConsistencyCheckOptionFix			0x10
#define kSTEConsistencyCheckOptionSend			0x20

- (NSUInteger) consistencyCheckOptionsFromString: (NSString *) optionsString
{
	NSArray *optionList = [optionsString componentsSeparatedByString: kSTEConsistencyCheckOptionSeparator];

	NSUInteger options = 0;

	if( [optionList count] > 0 ) {
		options =
		kSTEConsistencyCheckOptionRun * [optionList containsObject: kSTEConsistencyCheckOptionRunKey] |
		kSTEConsistencyCheckOptionFix * [optionList containsObject: kSTEConsistencyCheckOptionFixKey] |
		kSTEConsistencyCheckOptionAsk * [optionList containsObject: kSTEConsistencyCheckOptionAskKey] |
		kSTEConsistencyCheckOptionShow * [optionList containsObject: kSTEConsistencyCheckOptionShowKey] |
		kSTEConsistencyCheckOptionSend * [optionList containsObject: kSTEConsistencyCheckOptionSendKey] |
		kSTEConsistencyCheckOptionConfigured;
	}

	return options;
}

- (BOOL) consistencyCheckOrphanedObjectsForDocumentFromURL: (NSURL *) URL error: (NSError **) outError
{
	NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];

	NSDictionary *checks = [defaults dictionaryForKey: kSTEConsistencyCheckOrphanRelations];

	NSUInteger options = [self consistencyCheckOptionsFromString: [defaults stringForKey: kSTEConsistencyCheckOrphan]];

	if( !( options & kSTEConsistencyCheckOptionRun ) || 0 == [checks count] ) {
		return YES;
	}

	NSManagedObjectContext *moc = [self managedObjectContext];

	for( int iteration = 0 ; iteration < 3 ; ++iteration ) {

		NSMutableString *messages = [NSMutableString stringWithFormat: NSLocalizedString(@"alertConsistencyCheckIteration %ld\n", @""), (long)iteration];

		NSMutableArray *orphans = [NSMutableArray array];

		for( NSString *entityName in checks ) {
			NSString *ownerRelationshipName = [checks valueForKey: entityName];

			lpkdebug("consistency,heavy", entityName, ownerRelationshipName);

			NSFetchRequest *fetchRequest = [[[NSFetchRequest alloc] init] autorelease];
			NSEntityDescription *entity = [NSEntityDescription entityForName: entityName inManagedObjectContext: moc];
			NSPredicate *fetchPredicate = [NSPredicate predicateWithFormat:@"(%K == nil)", ownerRelationshipName];

			[fetchRequest setPredicate: fetchPredicate];
			[fetchRequest setEntity: entity];

			NSError *error = nil;
			NSArray *fetchResults = [moc executeFetchRequest:fetchRequest error: &error];

			if( nil == fetchResults ) {
				lpkerror("consistency", error);
				if( nil != outError ) {
					*outError = error;
				}
				return NO;
			}

			NSInteger count = [fetchResults count];

			for( NSManagedObject *object in fetchResults ) {
				lpkdebugf("consistency,heavy", "Orphaned %@ %@", entityName, [object respondsToSelector: @selector(name)] ? [object valueForKey: @"name"] : @"");
			}

			if( 0 != count ) {
				 [messages appendFormat: NSLocalizedString(@"alertOrphanedObjects: %ld ofType %@ without %@\n", @"repeating part of alert box message"), (long) count, entityName, ownerRelationshipName];
				 [orphans addObjectsFromArray: fetchResults];
			}
		}

		lpinfo(iteration, [orphans count]);

		if( 0 != [orphans count] ) {

			BOOL doFix = ( options & kSTEConsistencyCheckOptionFix ) ? YES : NO;

			if( options & kSTEConsistencyCheckOptionAsk ) {
				doFix = ( NSAlertDefaultReturn == NSRunCriticalAlertPanel(
														   NSLocalizedString(@"alertTitleDocumentFailedConsistencyCheck", @""),
														   @"%@",
														   NSLocalizedString(@"buttonDeleteOrphans_harmless", @""),
														   NSLocalizedString(@"buttonIgnoreOrphans", @""),
														   nil,
														   messages));
			} else {
				if( NO == doFix && ( options & kSTEConsistencyCheckOptionShow ) ) {
					NSRunInformationalAlertPanel(NSLocalizedString(@"alertTitleDocumentFailedConsistencyCheck", @""), @"%@", NSLocalizedString(@"buttonIgnoreOrphans", @""), nil, nil, messages);
				}
			}

			if( doFix ) {

				BOOL undoWasEnabled = [self enableUndo: NO];

				for(NSManagedObject *object in orphans ) {
					[moc deleteObject: object];
				}

				[self enableUndo: undoWasEnabled];

				if( options & kSTEConsistencyCheckOptionShow ) {
					NSRunInformationalAlertPanel(NSLocalizedString(@"alertTitleOrphansRemoved", @""), NSLocalizedString(@"alertTotalObjectsRemoved_includingChildren: %ld", @""), NSLocalizedString(@"buttonOK", @""), nil, nil, [[moc deletedObjects] count]);
				}

				if( options & kSTEConsistencyCheckOptionSend ) {
					[[NSApp storyTellingEditorDelegate] mailto: nil subject: @"orphaned objects" body: messages];
				}
			} else { // ! doFix
				break;
			}

		} else { // ! orphans count
			break;
		}

	} // for iteration

	return YES;
}

- (BOOL) consistencyCheckForDocumentFromURL: (NSURL *) URL error: (NSError **) outError
{
	BOOL success = YES;
	NSError *localError = nil;

	success = [self consistencyCheckOrphanedObjectsForDocumentFromURL: URL error: &localError];

	if( NO == success ) {
		[NSApp presentError: localError];
	}

	// don't stop loading - try as hard as we can

	return YES;
}

#pragma mark -
#pragma mark Saving

- (BOOL)writeToURL:(NSURL *)absoluteURL ofType:(NSString *)typeName error:(NSError **)outError {
	NSString *path = [[absoluteURL path] stringByDeletingLastPathComponent];
	BOOL isLocal = [FileUtil isLocalFile:path];
	CHECK_NSERROR_REASON_RETURN_NO(isLocal == YES, outError, NSPOSIXErrorDomain, EDEADLK, @"errorSavingToNonLocalVolumeProhibited");

	return [super writeToURL:absoluteURL ofType:typeName error:outError];
}

- (BOOL)writeToURL:(NSURL *)absoluteURL ofType:(NSString *)typeName forSaveOperation:(NSSaveOperationType)saveOperation originalContentsURL:(NSURL *)absoluteOriginalContentsURL error:(NSError **)outError {
	NSString *path = [[absoluteURL path] stringByDeletingLastPathComponent];
	BOOL isLocal = [FileUtil isLocalFile:path];
	CHECK_NSERROR_REASON_RETURN_NO(isLocal == YES, outError, NSPOSIXErrorDomain, EDEADLK, @"errorSavingToNonLocalVolumeProhibited");

	return [super writeToURL:absoluteURL ofType:typeName forSaveOperation:saveOperation originalContentsURL:absoluteOriginalContentsURL error:outError];
}

#pragma mark -
#pragma mark Document and snippet reading

- (BOOL)readFromData:(NSData *)data ofType:(NSString *)typeName toplevelObjects: (NSArray **) outToplevelObjects error:(NSError **)outError
{
	BOOL success = NO;

	if( [typeName isEqualToString: kDocumentTypeDictionary] || [typeName isEqualToString: kDocumentTypeSTPNGImport] ) {
		NSString *errorDescription = nil;

		// use NSPropertyListMutableContainers to allow for coordinate patching
		id plist = [NSPropertyListSerialization propertyListFromData: data mutabilityOption: NSPropertyListMutableContainers format: nil errorDescription: &errorDescription];

		CHECK_NSERROR_REASON_RETURN_NO(nil != plist, outError, NSPOSIXErrorDomain, EINVAL, @"errorCanNotParseDictionaryRepresentation");

		if( [typeName isEqualToString: kDocumentTypeSTPNGImport] ) {

			[self performSelector: @selector(delayedImportLayerDictionaries:) withObject: plist afterDelay: 0.0];
			success = YES;
		}

		if( [typeName isEqualToString: kDocumentTypeDictionary] ) {

			NSArray *toplevelObjects =  [[self class] importPlistRepresentation: plist intoManagedObjectContext: [self managedObjectContext] error: outError];

			if( nil != toplevelObjects ) {
				if( nil != outToplevelObjects ) {
					*outToplevelObjects = toplevelObjects;
				}
				success = YES;
			}
		}

	} else {
		success = [super readFromData:data ofType:typeName error:outError];
	}

	return success;
}

- (BOOL)readFromData:(NSData *)data ofType:(NSString *)typeName error:(NSError **)outError
{
	return [self readFromData: data ofType: typeName toplevelObjects: nil error: outError];
}

- (BOOL)readFromURL:(NSURL *)absoluteURL ofType:(NSString *)typeName toplevelObjects: (NSArray **) outToplevelObjects error:(NSError **)outError
{
	BOOL ret = NO;

	if( [typeName isEqualToString: kDocumentTypeDictionary] || [typeName isEqualToString: kDocumentTypeSTPNGImport] ) {

		NSData *data = [NSData dataWithContentsOfURL:absoluteURL options: NSUncachedRead error: outError];

		ret = data ? [self readFromData: data ofType: typeName toplevelObjects: outToplevelObjects error: outError] : NO;
	} else {

		if( [typeName isEqualToString: kDocumentTypePSD] || [typeName isEqualToString: kDocumentTypeTIFF]) {

			// do it a bit later so we have a window and can present a progress sheet
			[self performSelector: @selector(importLayersFromFileAtPath:) withObject: [absoluteURL path] afterDelay: 0.0];
			ret = YES;

		} else {
			ret = [super readFromURL:absoluteURL ofType:typeName error:outError];

			if( YES == ret ) {
				ret = [self consistencyCheckForDocumentFromURL: absoluteURL error: outError];
			}
		}
	}

	return ret;
}

- (BOOL)readFromURL:(NSURL *)absoluteURL ofType:(NSString *)typeName error:(NSError **)outError
{
	BOOL isLocal = [FileUtil isLocalFileURL:absoluteURL];
	CHECK_NSERROR_REASON_RETURN_NO(isLocal == YES, outError, NSPOSIXErrorDomain, EDEADLK, @"errorOpenFromNonLocalVolumeProhibited");

	return [self readFromURL: absoluteURL ofType: typeName toplevelObjects: nil error: outError];
}

// imports plist representations
// if defined STE_PLIST_COORDINATE_SCALING: with all coordinates (except the stage size) normalized to a 1 by 1 coordinate system
// else just a passthrough
+ (NSArray *) importPlistRepresentation: (NSArray *) plist intoManagedObjectContext: (NSManagedObjectContext *) managedObjectContext error: (NSError **) outError
{
#if STE_PLIST_COORDINATE_SCALING
	// keep it, maybe we need it for pluggable scripts later on
	ObjectExchangeObjectProcessor coordinateScaler = ^(NSManagedObject *obj, NSDictionary *options, NSInteger depth) {
		if([obj isKindOfClass: [SceneMO class]]) {
			SceneMO *scene = (SceneMO *)obj;
			StageMO *stage = scene.scenario.stage;
			CGSize stageSize = CGSizeMake( [stage.width floatValue], [stage.height floatValue] );
			//NSLog(@"stageSize %@", NSStringFromCGSize(stageSize));

			for( AssetMO *asset in scene.assets ) {
				//NSLog(@"scaling %@", asset.name);
				[asset.keyframeAnimation scaleGeometricPropertiesBy: stageSize];
			}
		}
	};

	NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
							 [NSArray arrayWithObject: [[coordinateScaler copy] autorelease]], kObjectExchangeObjectProcessors,
							 nil];
#else
	NSDictionary *options = nil;
#endif

	NSArray *objects = [NSManagedObject objectsFromDictionaryRepresentations: plist inManagedObjectContext: managedObjectContext options: options error: outError];

	return objects;
}

// uses readFromURL, therefore scales normalized coordinates if STE_PLIST_COORDINATE_SCALING is defined
- (NSArray *) objectsFromPlistResourceNamed: (NSString *) name error:(NSError **)outError
{
	NSArray *toplevelObjects = nil;

	NSString *plistPath = [[NSBundle bundleForClass: [self class]] pathForResource: name ofType: @"plist"];

	CHECK_NSERROR_REASON_RETURN_NO(plistPath, outError, NSPOSIXErrorDomain, ENOENT, @"errorPlistResourceNotFound: %@", name);

	if( NO == [self readFromURL: [NSURL fileURLWithPath: plistPath] ofType: kDocumentTypeDictionary toplevelObjects: &toplevelObjects error: outError] ) {
		return nil;
	}

	return toplevelObjects;
}

#pragma mark -
#pragma mark Scene Prototype creation

- (SceneMO*)createScenePrototype {
	NSError *outError = nil;
	SceneMO *scene = [[self objectsFromPlistResourceNamed: kAFPSDImportPrototypeSceneName error: &outError] lastObject];
	return scene;
}

#pragma mark -
#pragma mark Layer Dictionary Import

- (void) delayedImportLayerDictionaries: (NSArray *) layerDictionaries
{
	NSError *error = nil;
	if( NO == [self importLayerDictionaries: layerDictionaries error: &error] ) {
		[NSApp presentError: error];
	}
}

- (BOOL) importLayerDictionaries: (NSArray * ) layerDictionaries error: (NSError **) outError;
{
	BOOL success = YES;

	NSManagedObjectContext *moc = [self managedObjectContext];

	[moc processPendingChanges];
    [[moc undoManager] disableUndoRegistration];

	StageMO *stage = [self ensureStageAndSceneForImportOfLayerDictionaries: layerDictionaries error: outError];

	// BK: when we start importing into existing documents we should make a decision about undo registration
	// based on whether the above call created a stage or not (it will need a BOOL * argument)

	do {
		if( nil == stage ) {
			success = NO;
			break;
		}

		SceneMO *scene = [[stage valueForKeyPath: @"scenario.playables"] anyObject];

		NSAssert( scene != nil, @"no scene in import");

		NSMutableSet *mutableAssets = [scene mutableSetValueForKey: @"assets"];

		for(NSDictionary *layerDictionary in layerDictionaries) {

			if( -1 == [[layerDictionary valueForKey: @"zPosition"] integerValue] ) { // skip composite layer
				continue;
			}

			AssetMO *asset = [[self objectsFromPlistResourceNamed: kStoryTellingPrototypeAssetName error: outError] lastObject];

			if( nil == asset || NO == [self configureAsset: asset fromLayerDictionary: layerDictionary error: outError] ) {
				success = NO;
				break;
			}

			[mutableAssets addObject: asset];

			if ([asset.primaryBlob.externalURL length] > 0) {
				NSURL *URL = [NSURL URLWithString: asset.primaryBlob.externalURL];
				if (URL != nil) {
					if ([[AFCache sharedInstance] hasCachedItemForURL:URL]) {
						[[AFCache sharedInstance] purgeCacheableItemForURL:URL];
					}
				}
			}
		}
	} while(0);

	[moc processPendingChanges];
    [[moc undoManager] enableUndoRegistration];

	return success;
}

- (StageMO *) ensureStageAndSceneForImportOfLayerDictionaries: (NSArray * ) layerDictionaries error: (NSError **) outError
{
	NSRect documentRect = NSZeroRect;
	NSRect stageRect = NSZeroRect;

	//NSMutableArray *offendingNames = [NSMutableArray array];

	for(NSDictionary *layerProperties in layerDictionaries) {
		NSRect layerRect = NSRectFromString([layerProperties valueForKey: @"frame"]);
		stageRect = NSUnionRect(stageRect,  layerRect);

		if( -1 == [[layerProperties valueForKey: @"zPosition"] integerValue] ) {
			documentRect = layerRect;
		}
	}

#ifdef STE_WARN_ON_NEGATIVE_LAYER_OFFSET
	if( NSMinX(stageRect) < 0 || NSMinY(stageRect) < 0 ) {
		NSAlert *alert = [NSAlert alertWithMessageText: NSLocalizedString(@"warningPSDImportHasNegativeLayerOffsetsTitle", @"alert box text") defaultButton: NSLocalizedString(@"ignoreAndImportAsIsButtonTitle", @"") alternateButton:  NSLocalizedString(@"auto-correct will follow", @"") otherButton: nil informativeTextWithFormat:
		NSLocalizedString(@"warningPSDImportHasNegativeLayerOffsetsText", @"")];
		[alert runModal];
	}
#endif

	if( NO == NSEqualRects( NSZeroRect, documentRect) ) {
		stageRect = documentRect;
	}

	double psdImportScale = self.psdImportScale;

	stageRect.origin.x *= psdImportScale;
	stageRect.origin.y *= psdImportScale;
	stageRect.size.width *= psdImportScale;
	stageRect.size.height *= psdImportScale;

	NSLog(@"stageRect: %@", NSStringFromRect(stageRect));

	StageMO *stage = MODEL_ACCESS(kModelAccessStageSelectionKeyPath, @"self");

	if( NSNoSelectionMarker == stage ) {

		NSLog(@"constructing stage");

		stage = [[self objectsFromPlistResourceNamed: kAFPSDImportPrototypeDocumentName error: outError] lastObject];

		if( nil == stage ) {
			return nil;
		}

		[stage setValue: [NSNumber numberWithInteger: NSWidth(stageRect)] forKey: @"width"];
		[stage setValue: [NSNumber numberWithInteger: NSHeight(stageRect)] forKey: @"height"];
	}

	// returns array, has 1 scene
	SceneMO *scene = [[self objectsFromPlistResourceNamed: kAFPSDImportPrototypeSceneName error: outError] lastObject];

	if( nil == scene ) {
		return nil;
	}

	[[stage.scenario mutableSetValueForKey: @"playables"] addObject: scene];

	return stage;
}

- (BOOL) configureAsset: (AssetMO *) asset fromLayerDictionary: (NSDictionary *) layerDictionary error: (NSError **) outError
{
	CGRect frame = CGRectFromString([layerDictionary valueForKey: @"frame"]);

	double psdImportScale = self.psdImportScale;

	frame.origin.x *= psdImportScale;
	frame.origin.y *= psdImportScale;
	frame.size.width *= psdImportScale;
	frame.size.height *= psdImportScale;

	asset.name = [layerDictionary valueForKey: @"name"];

	NSString *externalURL = [layerDictionary valueForKey: @"lastExportURL"];

	if( nil == externalURL ) {
		NSString *importPath = [[layerDictionary valueForKey: @"importPath"] stringByExpandingTildeInPath];
		if( nil != importPath ) {
			externalURL = [[NSURL fileURLWithPath: importPath] absoluteString];
		}
	}

	NSDictionary *externalDataProperties = ER_DICT(
												   @"keyName", kSTEBlobRefKeyPrimary,
												   @"externalURL", externalURL,
   												   @"externalId", @"none",
												   @"contentType", (id)kUTTypeImage,
												   );

	asset.primaryBlob = [self externalDataWithProperties: externalDataProperties];

	asset.viewPosition = [layerDictionary valueForKey: @"zPosition"];

	NSNumber *hidden = [layerDictionary valueForKey: @"hidden"];

	asset.hidden = (nil == hidden) ? [NSNumber numberWithBool: NO] : hidden;

	KeyframeMO *keyframe = [[asset valueForKeyPath: @"keyframeAnimation.keyframes"] anyObject];

	[keyframe setValue: [NSValue valueWithCGPoint: frame.origin] forKey: @"position"];

	frame.origin = CGPointZero;
	[keyframe setValue: [NSValue valueWithCGRect: frame] forKey: @"bounds"];

	return YES;
}

- (void) startImportTaskWithCLITool: (NSString *) cliTool arguments: (NSArray *) arguments
{
	NSBundle *myBundle = [NSBundle bundleForClass: [self class]];

	NSString *importerBundlePath = [myBundle pathForResource: kAFPSDImportBundleName ofType: kAFPSDImportBundleExtension];
	NSAssert(nil != importerBundlePath, @"missing importer bundle");

	NSBundle *importerBundle = [NSBundle bundleWithPath: importerBundlePath];
	NSAssert(nil != importerBundle, @"can not create importer bundle");

	NSString *importerCLIToolPath = [importerBundle pathForAuxiliaryExecutable: cliTool];
	NSAssert(nil != importerCLIToolPath, @"can not locate cli tool in importer bundle");

	NSArray *importerArguments = [[NSArray arrayWithObject: importerCLIToolPath] arrayByAddingObjectsFromArray: arguments];

	self.importTaskWrapper = [[[TaskWrapper alloc] initWithController: self arguments: importerArguments userInfo: nil] autorelease];
	[self.importTaskWrapper startProcess];
}

- (void) importLayersFromFileAtPath: (NSString *) path
{
	NSString *theCLITool = nil;
	NSUserDefaults *currentDefaults = [NSUserDefaults standardUserDefaults];
	if ([[currentDefaults objectForKey:kSTEUseImageMagick] boolValue] == YES) {
		theCLITool = kAFPSDImportBundleCLIToolImageMagick;
	} else {
		theCLITool = kAFPSDImportBundleCLIToolXee;
	}
	[self startImportTaskWithCLITool: theCLITool arguments: [NSArray arrayWithObject: path]];
}

- (void) startModalProgress: (NSString *) title
{
#if TOGGLE_OBSERVERS
	[self disableObservers];
#endif
	[self.progressTitleField setStringValue: title];
	[self.progressIndicator startAnimation: self];

	[NSApp beginSheet: self.progressSheet modalForWindow: self.editorWindow modalDelegate: self didEndSelector: @selector(progressSheetDidEnd:returnCode:contextInfo:) contextInfo: nil];
}

- (void) progressSheetDidEnd:(NSWindow *)sheet returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo;
{
	[self.progressIndicator stopAnimation: self];
	[sheet orderOut:self];
}

- (void) endModalProgress
{
	[NSApp endSheet: self.progressSheet];
#if TOGGLE_OBSERVERS
	[self enableObservers];
#endif
}

- (void) processStarted
{
	NSLog(@"processStarted");
	[self startModalProgress: NSLocalizedString(@"psdImportInProgress", @"")];
}

- (void) processFinishedWithStatus: (NSInteger) terminationStatus reason: (NSTaskTerminationReason) terminationReason outputData: (NSData *) outputData errorData: (NSData *) errorData userInfo: (id) info
{
	NSError *error = nil;

	BOOL showError = NO;
	BOOL showOutput = NO;

	NSString *basename = [NSString stringWithFormat: @"AFImport-%f", [NSDate timeIntervalSinceReferenceDate]];

	NSString *errorFile = [NSTemporaryDirectory() stringByAppendingPathComponent: [basename stringByAppendingPathExtension: @"log"]];
	NSString *outputFile = [NSTemporaryDirectory() stringByAppendingPathComponent: [basename stringByAppendingPathExtension: @"plist"]];

	NSLog(@"importer finished - status: %ld reason: %ld log: %@ output: %@", (long) terminationStatus, (long) terminationReason, errorFile, outputFile);

	if( NO == [errorData writeToFile: errorFile atomically: YES] || NO == [outputData writeToFile: outputFile atomically: YES] ) {
		SET_NSERROR_REASON(&error, NSPOSIXErrorDomain, EINVAL, @"errorPSDImportCouldNotWriteDebugOutput");
	} else {

		if( NSTaskTerminationReasonExit == terminationReason && 0 == terminationStatus ) {

			NSArray *layerDictionaries = [NSPropertyListSerialization propertyListWithData: outputData options: 0 format: nil error: &error];

			if( nil == layerDictionaries ) {
				SET_NSERROR_REASON(&error, NSPOSIXErrorDomain, EINVAL, @"errorPSDImportProducedUnparseableData");
				showError = showOutput = YES;
			} else {

				if( 0 == [layerDictionaries count] ) {
					SET_NSERROR_REASON(&error, NSPOSIXErrorDomain, EINVAL, @"errorPSDImportProducedZeroLayers");
					showError = showOutput = YES;
				} else {

					if( NO == [self importLayerDictionaries: layerDictionaries error: &error] ) {
						showOutput = YES;
					}
				}

			}

		} else {
			SET_NSERROR_REASON(&error, NSPOSIXErrorDomain, EINVAL, @"errorPSDImportFailed");
			showError = YES;
		}
	}

	[self endModalProgress];

	if( nil != error ) {
		[self presentError: error];
	}

	if( showOutput || showError ) {
		system([[NSString stringWithFormat: @"open -e %@ %@", showError ? errorFile : @"", showOutput ? outputFile : @""] UTF8String]);
	}

	// delay release so we don't destruct our caller
	[self performSelector: @selector(setImportTaskWrapper:) withObject: nil afterDelay: 0.0];
}

@end


text:
module Lambency.Loaders.OBJLoader (
  OBJInfo(..),

  OBJGeometry,
  OBJOutput(..),

  obj2V3Mesh,
  obj2OV3Mesh,
  obj2TV3Mesh,
  obj2OTV3Mesh,

  loadOBJ,
) where

--------------------------------------------------------------------------------
import Lambency.Loaders.Utils
import Lambency.Mesh
import Lambency.Vertex

import qualified Data.Map as Map

import Data.Char (toLower)
import Data.List (sortBy)
import Data.Maybe (isJust)
import Data.Ord (comparing)
import Data.Text (pack)

import Data.Array.Unboxed (UArray, listArray, (!))

import Control.Monad.State.Strict as State

#if __GLASGOW_HASKELL__ <= 708
import Control.Applicative hiding (many, (<|>))
#endif

import Text.Parsec
import Text.Parsec.Text (Parser)

import Linear.Metric
import Linear.Vector
import Linear.V2
import Linear.V3
--------------------------------------------------------------------------------

type OBJVertex = Vec3f
type OBJVertexList = [OBJVertex]

type OBJTexCoord = Vec2f
type OBJTexCoordList = [OBJTexCoord]

-- type OBJNormal = Normal3
type OBJNormal = Vec3f
type OBJNormalList = [OBJNormal]

type OBJIndex = (Int, Maybe Int, Maybe Int) -- derives Eq, Ord
type OBJIndexList = [OBJIndex]
type OBJFace = OBJIndexList
type OBJFaceList = [OBJFace]

data OBJGeometry = OBJGeometry {
  objVerts :: OBJVertexList,
  objTexCoords :: OBJTexCoordList,
  objNormals :: OBJNormalList,
  objFaces :: OBJFaceList
} deriving (Show)

data OBJInfo = OBJInfo {
  mtlLib :: FilePath,
  mtlName :: String,
  numVerts :: Int,
  numTexCoords :: Int,
  numNormals :: Int,
  numFaces :: Int
} deriving (Show, Ord, Eq)

data OBJOutput = OBJOutput {
  objMtlLib :: FilePath,
  objMeshes :: [(OBJInfo, OBJGeometry)]
}

convertNegativeFaces :: OBJInfo -> OBJFace -> OBJFace
convertNegativeFaces geomLen = map (convertNegativeIndex geomLen)
  where
    convert :: Int -> Int -> Int
    convert len x
      | x < 0 = len + x + 1
      | otherwise = x

    convertNegativeIndex :: OBJInfo -> OBJIndex -> OBJIndex
    convertNegativeIndex info (p, tc, n) =
      (convert (numVerts info) p,
       liftM (convert . numTexCoords $ info) tc,
       liftM (convert . numNormals $ info) n)

reverseGeometry :: OBJGeometry -> OBJGeometry
reverseGeometry geom = OBJGeometry {
  objVerts = reverse $ objVerts geom,
  objTexCoords = reverse $ objTexCoords geom,
  objNormals = reverse $ objNormals geom,
  objFaces = objFaces geom
  }

triangulate :: OBJFaceList -> OBJIndexList
triangulate fs = let
  tglte :: OBJFace -> [OBJFace] -> [OBJFace]
  tglte f faces
    | length f <= 3 = f : faces
    | otherwise =
      case f of
        (i1 : i2 : i3 : rest) -> tglte (i1 : i3 : rest) ([i1, i2, i3] : faces)
        _ -> error "Wat"
  in
   concat . concat $ map (flip tglte []) fs

mkVec2fLookup :: [Vec2f] -> (Int -> Vec2f)
mkVec2fLookup vecs = let
  arr :: UArray Int Float
  arr = listArray (1, length vecs * 2) $
        concat $ map (\(V2 x y) -> [x, y]) vecs

  in (\i -> V2 (arr ! (2*i - 1)) (arr ! (2 * i)))

mkVec3fLookup :: [Vec3f] -> (Int -> Vec3f)
mkVec3fLookup vecs = let
  arr :: UArray Int Float
  arr = listArray (1, length vecs * 3) $
        concat $ map (\(V3 x y z) -> [x, y, z]) vecs

  in \i -> V3 (arr ! (3*i - 2)) (arr ! (3*i - 1)) (arr ! (3 * i))

genIdxMap' :: Vertex a => (OBJIndex -> a) -> OBJIndexList -> Map.Map OBJIndex (Int, a) -> Int ->
             Map.Map OBJIndex (Int, a)
genIdxMap' _ [] m _ = m
genIdxMap' f (idx : rest) m nVerts =
  case Map.lookup idx m of
    Just _ -> genIdxMap' f rest m nVerts
    Nothing -> genIdxMap' f rest (Map.insert idx (nVerts, f idx) m) (nVerts + 1)

genIdxMap :: Vertex a => (OBJIndex -> a) -> OBJIndexList -> Map.Map OBJIndex (Int, a)
genIdxMap f idxs = genIdxMap' f idxs Map.empty 0

genMesh :: Vertex a => (OBJIndex -> a) -> OBJIndexList -> Mesh a
genMesh f idxs = let
  idxMap = genIdxMap f idxs
  in Mesh {
    vertices = map snd $ sortBy (comparing fst) $ Map.elems idxMap,
    indices = map (fromIntegral . fst . (idxMap Map.!)) idxs
  }

normalObj2Mesh :: OBJVertexList -> OBJNormalList -> OBJFaceList -> Mesh OVertex3
normalObj2Mesh verts normals faces = let
  ns = mkVec3fLookup normals
  vs = mkVec3fLookup verts

  idx2Vertex :: OBJIndex -> OVertex3
  idx2Vertex (x, _, Just n) = mkNormVertex3 (vs x) (ns n)
  idx2Vertex i = error $ "Ill formatted OV3 index: " ++ (show i)

  in genMesh idx2Vertex (triangulate faces)

texturedObj2Mesh :: OBJVertexList -> OBJTexCoordList -> OBJFaceList -> Mesh TVertex3
texturedObj2Mesh verts texcoords faces = let
  tcs = mkVec2fLookup texcoords
  vs = mkVec3fLookup verts

  idx2Vertex :: OBJIndex -> TVertex3
  idx2Vertex (x, Just tc, _) = mkTexVertex3 (vs x) (tcs tc)
  idx2Vertex i = error $ "Ill formatted TV3 index: " ++ (show i)

  in genMesh idx2Vertex (triangulate faces)

normTexturedObj2Mesh :: OBJVertexList -> OBJTexCoordList -> OBJNormalList -> OBJFaceList ->
                        Mesh OTVertex3
normTexturedObj2Mesh verts texcoords normals faces = let
  ns = mkVec3fLookup normals
  tcs = mkVec2fLookup texcoords
  vs = mkVec3fLookup verts

  idx2Vertex :: OBJIndex -> OTVertex3
  idx2Vertex (x, Just tc, Just n) = mkNormTexVertex3 (vs x) (ns n) (tcs tc)
  idx2Vertex i = error $ "Ill formatted OTV3 index: " ++ (show i)

  in genMesh idx2Vertex (triangulate faces)

obj2V3Mesh :: OBJGeometry -> Mesh Vertex3
obj2V3Mesh (OBJGeometry {objVerts=vs, objTexCoords=_, objNormals=_, objFaces=fs}) =
  let vfn = mkVec3fLookup vs
      idx2Vertex (x, _, _) = mkVertex3 (vfn x)
  in genMesh idx2Vertex (triangulate fs)

obj2OV3Mesh :: OBJGeometry -> Mesh OVertex3
obj2OV3Mesh (OBJGeometry {objVerts=vs, objTexCoords=_, objNormals=ns, objFaces = fs})
  | null ns = normalObj2Mesh vs (repeat zero) fs
  | otherwise = normalObj2Mesh vs ns fs

obj2TV3Mesh :: OBJGeometry -> Mesh TVertex3
obj2TV3Mesh (OBJGeometry {objVerts=vs, objTexCoords=uvs, objNormals=_, objFaces = fs})
  | null uvs = texturedObj2Mesh vs (repeat zero) fs
  | otherwise = texturedObj2Mesh vs uvs fs

obj2OTV3Mesh :: OBJGeometry -> Mesh OTVertex3
obj2OTV3Mesh (OBJGeometry {objVerts=vs, objTexCoords=uvs, objNormals=ns, objFaces = fs})
  | (null ns) && (null uvs) =
    normTexturedObj2Mesh vs (repeat zero) (repeat zero) fs
  -- !FIXME! Do we want to generate normals here maybe?
  | null ns = normTexturedObj2Mesh vs uvs (repeat zero) fs
  | null uvs = normTexturedObj2Mesh vs (repeat zero) ns fs
  | otherwise = normTexturedObj2Mesh vs uvs ns fs

data Command = Normal Vec3f
             | Position Vec3f
             | TexCoord Vec2f
             | Face OBJFace
             | MtlLib FilePath
             | Group String
             | MtlName String
             deriving (Show, Eq, Ord)

initialGeom :: OBJGeometry
initialGeom = OBJGeometry [] [] [] []

initialInfo :: OBJInfo
initialInfo = OBJInfo "" "" 0 0 0 0

checkInfo :: OBJFaceList -> OBJInfo -> OBJInfo
checkInfo [] x = x
checkInfo faces info =
  let hasTexCoords = any (any (\(_, x, _) -> isJust x)) faces
      hasNormals = any (any (\(_, _, x) -> isJust x)) faces

      fixedTCInfo
        | hasTexCoords = info
        | otherwise = info { numTexCoords = 0 }

      fixedInfo
        | hasNormals = fixedTCInfo
        | otherwise = fixedTCInfo { numNormals = 0 }

  in fixedInfo

addCommand :: Command -> State.State (OBJInfo, OBJGeometry) [(OBJInfo, OBJGeometry)]
addCommand (Normal n) = do
  (info, geom) <- get
  let newInfo = info { numNormals = numNormals info + 1 }
      newGeom = geom { objNormals = signorm n : (objNormals geom) }
  put (newInfo, newGeom)
  return []

addCommand (Position p) = do
  (info, geom) <- get
  let newInfo = info { numVerts = numVerts info + 1 }
      newGeom = geom { objVerts = p : (objVerts geom) }
  put (newInfo, newGeom)
  return []

addCommand (TexCoord tc) = do
  (info, geom) <- get
  let newInfo = info { numTexCoords = numTexCoords info + 1 }
      newGeom = geom { objTexCoords = tc : (objTexCoords geom) }
  put (newInfo, newGeom)
  return []

addCommand (Face f) = do
  (info, geom) <- get
  let newInfo = info { numFaces = numFaces info + 1 }
      newGeom = geom { objFaces = convertNegativeFaces info f : (objFaces geom) }
  put (newInfo, newGeom)
  return []

addCommand (MtlLib lib) = do
  (info, geom) <- get
  let newInfo = info { mtlLib = lib }
  put (newInfo, geom)
  return []

addCommand (Group _) = return []

addCommand (MtlName mtl) = do
  (info, geom) <- get
  let newInfo = info { mtlName = map toLower mtl }
  case objFaces geom of
    [] -> put (newInfo, geom) >> return []
    faces -> do
      let newGeom = geom { objFaces = [] }
          newInfo' = newInfo { numFaces = 0 }
      put (newInfo', newGeom)
      return [(checkInfo faces info, reverseGeometry geom)]

-- addCommand c = error $ "Lambency.Loaders.OBJLoader (addCommand): Unable to handle command " ++ show c

runCommands :: [Command] -> [(OBJInfo, OBJGeometry)]
runCommands cmds =
  let (pairs, (lastInfo, lastGeom)) = State.runState (mapM addCommand cmds) (initialInfo, initialGeom)
  in (checkInfo (objFaces lastGeom) lastInfo, reverseGeometry lastGeom) : (concat pairs)

parseFile :: Parser [(OBJInfo, OBJGeometry)]
parseFile = let

  ignoreRestOfLine :: Parser ()
  ignoreRestOfLine = many (noneOf "\r\n") >> endOfLine >> return ()

  comment :: Parser ()
  comment = char '#' >> ignoreRestOfLine

  -- FIXME --
  errata :: Parser ()
  errata = (oneOf "os" >> ignoreRestOfLine)

  blankLine :: Parser ()
  blankLine = (endOfLine <|>
               (skipMany1 (tab <|> char ' ') >> endOfLine)) >> return ()

  vert :: Parser Command
  vert = do
    v <- char 'v' >>
         ((many1 space >> (Position <$> vector3))
          <|> (char 'n' >> (Normal <$> vector3))
          <|> (char 't' >> (TexCoord <$> vector2)))
    _ <- many (noneOf "\r\n")
    return v

  integer :: Parser Int
  integer = do
    skipMany (tab <|> char ' ')
    m <- option 1 $ do
      _ <- char '-'
      return (-1)
    v <- many1 digit
    return $ m * (read v)

  index :: Parser OBJIndex
  index = do
    skipMany (tab <|> char ' ')
    idx <- integer
    (tc, n) <- (do _ <- char '/'
                   mtc <- option Nothing $ Just <$> integer
                   mn <- (char '/' >> (Just <$> integer)) <|> (return Nothing)
                   return (mtc, mn))
               <|>
               (return (Nothing, Nothing))
    skipMany (tab <|> char ' ')
    return (idx, tc, n)

  face :: Parser Command
  face = do
    idxs <- char 'f' >> (many1 index)
    _ <- many (noneOf "\r\n")
    return $ Face idxs

  mtllib :: Parser Command
  mtllib = MtlLib <$> (string "mtllib" >> spaces >> many1 (noneOf " #\n\r\t"))

  usemtl :: Parser Command
  usemtl = MtlName <$> (string "usemtl" >> spaces >> many1 (noneOf " #\n\r\t"))

  meshGroup :: Parser Command
  meshGroup = Group <$> (char 'g' >> spaces >> many1 (noneOf " #\n\r\t"))

  command :: Parser Command
  command = vert <|> face <|> mtllib <|> usemtl <|> meshGroup

  ignorableLines :: Parser ()
  ignorableLines = many (errata <|> comment <|> blankLine) >> return ()

  parseLine :: Parser Command
  parseLine = do
    v <- command
    ignorableLines
    return v

  in do
    ignorableLines
    cmds <- many1 parseLine
    _ <- try (ignorableLines >> eof) >> return ()
    return $ runCommands cmds

parseOBJ :: ([(OBJInfo, OBJGeometry)] -> a) -> FilePath -> IO a
parseOBJ fn filepath = do
  s <- readFile filepath
  case parse parseFile filepath (pack s) of
    Left x -> error $ show x
    Right [] -> error "Lambency.Loaders.OBJLoader (parseOBJ): OBJ file had no meshes!"
    Right xs -> return $ fn xs

genOutput :: [(OBJInfo, OBJGeometry)] -> OBJOutput
genOutput [] = OBJOutput "" []
genOutput o@((info', _):_) =
  let mtllib = mtlLib info'

      pruneOutput (info, _) = mtlLib info /= mtllib

      addOutput [] result = result
      addOutput (x : rest) result =
        if pruneOutput x
        then addOutput rest result
        else addOutput rest (x : result)

  in OBJOutput mtllib (addOutput o [])

loadOBJ :: FilePath -> IO OBJOutput
loadOBJ = parseOBJ genOutput

redpajama_set_name: RedPajamaGithub


text:
package com.java110.job.adapt.roomRenovation;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.java110.core.factory.WechatFactory;
import com.java110.dto.community.CommunityDto;
import com.java110.dto.owner.OwnerAppUserDto;
import com.java110.dto.owner.OwnerDto;
import com.java110.dto.owner.OwnerRoomRelDto;
import com.java110.dto.roomRenovation.RoomRenovationDto;
import com.java110.dto.roomRenovationDetail.RoomRenovationDetailDto;
import com.java110.dto.smallWeChat.SmallWeChatDto;
import com.java110.dto.smallWechatAttr.SmallWechatAttrDto;
import com.java110.entity.order.Business;
import com.java110.entity.wechat.Content;
import com.java110.entity.wechat.Data;
import com.java110.entity.wechat.PropertyFeeTemplateMessage;
import com.java110.intf.community.ICommunityInnerServiceSMO;
import com.java110.intf.community.IRoomRenovationDetailInnerServiceSMO;
import com.java110.intf.store.ISmallWeChatInnerServiceSMO;
import com.java110.intf.store.ISmallWechatAttrInnerServiceSMO;
import com.java110.intf.user.IOwnerAppUserInnerServiceSMO;
import com.java110.intf.user.IOwnerInnerServiceSMO;
import com.java110.intf.user.IOwnerRoomRelInnerServiceSMO;
import com.java110.intf.user.IRoomRenovationsInnerServiceSMO;
import com.java110.job.adapt.DatabusAdaptImpl;
import com.java110.utils.cache.MappingCache;
import com.java110.utils.util.StringUtil;
import org.slf4j.Logger;
import com.java110.core.log.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;

import java.text.SimpleDateFormat;
import java.util.List;

/**
 * 房屋验收通知适配器
 *
 * @author fqz
 * @date 2021-03-01 13:50
 */
@Component(value = "machineRoomRenovationDetail")
public class MachineRoomRenovationDetail extends DatabusAdaptImpl {

    @Autowired
    private ICommunityInnerServiceSMO communityInnerServiceSMO;

    @Autowired
    private IRoomRenovationsInnerServiceSMO roomRenovationsInnerServiceSMO;

    @Autowired
    private IRoomRenovationDetailInnerServiceSMO roomRenovationDetailInnerServiceSMO;

    @Autowired
    private ISmallWeChatInnerServiceSMO smallWeChatInnerServiceSMOImpl;

    @Autowired
    private ISmallWechatAttrInnerServiceSMO smallWechatAttrInnerServiceSMOImpl;

    @Autowired
    private IOwnerRoomRelInnerServiceSMO ownerRoomRelInnerServiceSMO;

    @Autowired
    private IOwnerInnerServiceSMO ownerInnerServiceSMO;

    @Autowired
    private IOwnerAppUserInnerServiceSMO ownerAppUserInnerServiceSMO;

    @Autowired
    private RestTemplate outRestTemplate;

    private static Logger logger = LoggerFactory.getLogger(MachineRoomRenovationDetail.class);

    //模板信息推送地址
    private static String sendMsgUrl = "https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=";

    @Override
    public void execute(Business business, List<Business> businesses) {
        JSONObject data = business.getData();
        //查询小区信息
        CommunityDto communityDto = new CommunityDto();
        communityDto.setCommunityId(data.getString("communityId"));
        List<CommunityDto> communityDtos = communityInnerServiceSMO.queryCommunitys(communityDto);
        RoomRenovationDto renovationDto = new RoomRenovationDto();
        renovationDto.setrId(data.getString("rId"));
        //查询房屋装修信息
        List<RoomRenovationDto> roomRenovationDtoList = roomRenovationsInnerServiceSMO.queryRoomRenovations(renovationDto);
        //查询房屋装修验收信息
        RoomRenovationDetailDto roomRenovationDetailDto = new RoomRenovationDetailDto();
        roomRenovationDetailDto.setrId(data.getString("rId"));
        List<RoomRenovationDetailDto> roomRenovationDetailDtos = roomRenovationDetailInnerServiceSMO.queryRoomRenovationDetails(roomRenovationDetailDto);
        //获得联系人信息
        String personName = roomRenovationDtoList.get(0).getPersonName();
        //获得装修房屋号
        String roomName = roomRenovationDtoList.get(0).getRoomName();
        //获取房屋id
        String roomId = data.getString("roomId");
        //获得状态
        String state = data.getString("state");
        //获取状态名称
        String stateName = roomRenovationDtoList.get(0).getStateName();
        //获取当前用户id
        String userId = data.getString("userId");
        String stateMark = "";
        if (state.equals("5000")) {
            stateMark = "通过";
        } else if (state.equals("6000")) {
            stateMark = "不通过";
        }
        //获得验收备注
        String remark = data.getString("remark");
        //获取验收时间
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String createTime = sdf.format(roomRenovationDetailDtos.get(0).getCreateTime());
        JSONObject paramIn = new JSONObject();
        paramIn.put("personName", personName);
        paramIn.put("stateMark", stateMark);
        paramIn.put("state", state);
        paramIn.put("stateName", stateName);
        paramIn.put("remark", remark);
        paramIn.put("createTime", createTime);
        paramIn.put("roomName", roomName);
        paramIn.put("roomId", roomId);
        paramIn.put("userId", userId);
        //给业主推送信息
        sendMessage(paramIn, communityDtos.get(0));
    }

    /**
     * 验收成功/失败给业主推送消息
     *
     * @param paramIn
     * @param communityDto
     */
    private void sendMessage(JSONObject paramIn, CommunityDto communityDto) {
        //查询公众号配置
        SmallWeChatDto smallWeChatDto = new SmallWeChatDto();
        smallWeChatDto.setWeChatType("1100");
        smallWeChatDto.setObjType(SmallWeChatDto.OBJ_TYPE_COMMUNITY);
        smallWeChatDto.setObjId(communityDto.getCommunityId());
        List<SmallWeChatDto> smallWeChatDtos = smallWeChatInnerServiceSMOImpl.querySmallWeChats(smallWeChatDto);
        if (smallWeChatDto == null || smallWeChatDtos.size() <= 0) {
            logger.info("未配置微信公众号信息,定时任务执行结束");
            return;
        }
        SmallWeChatDto weChatDto = smallWeChatDtos.get(0);
        SmallWechatAttrDto smallWechatAttrDto = new SmallWechatAttrDto();
        smallWechatAttrDto.setCommunityId(communityDto.getCommunityId());
        smallWechatAttrDto.setWechatId(weChatDto.getWeChatId());
        smallWechatAttrDto.setSpecCd(SmallWechatAttrDto.SPEC_CD_WECHAT_HOUSE_DECORATION_COMPLETED_TEMPLATE);
        List<SmallWechatAttrDto> smallWechatAttrDtos = smallWechatAttrInnerServiceSMOImpl.querySmallWechatAttrs(smallWechatAttrDto);
        if (smallWechatAttrDtos == null || smallWechatAttrDtos.size() <= 0) {
            logger.info("未配置微信公众号消息模板");
            return;
        }
        String templateId = smallWechatAttrDtos.get(0).getValue();
        String accessToken = WechatFactory.getAccessToken(weChatDto.getAppId(), weChatDto.getAppSecret());
        if (StringUtil.isEmpty(accessToken)) {
            logger.info("推送微信模板,获取accessToken失败:{}", accessToken);
            return;
        }
        //获取房屋id
        String roomId = paramIn.getString("roomId");
        OwnerRoomRelDto ownerRoomRelDto = new OwnerRoomRelDto();
        ownerRoomRelDto.setRoomId(roomId);
        //查询业主房屋关系表
        List<OwnerRoomRelDto> ownerRoomRelDtos = ownerRoomRelInnerServiceSMO.queryOwnerRoomRels(ownerRoomRelDto);
        //取得业主id
        String ownerId = ownerRoomRelDtos.get(0).getOwnerId();
        OwnerDto ownerDto = new OwnerDto();
        ownerDto.setOwnerId(ownerId);
        //1001 业主本人 1002 家庭成员
        ownerDto.setOwnerTypeCd("1001");
        //查询业主
        List<OwnerDto> ownerDtos = ownerInnerServiceSMO.queryOwners(ownerDto);
        //获得成员id
        String memberId = ownerDtos.get(0).getMemberId();
        OwnerAppUserDto ownerAppUserDto = new OwnerAppUserDto();
        ownerAppUserDto.setMemberId(memberId);
        ownerAppUserDto.setAppType("WECHAT");
        //查询绑定业主
        List<OwnerAppUserDto> ownerAppUserDtos = ownerAppUserInnerServiceSMO.queryOwnerAppUsers(ownerAppUserDto);
        //查询当前绑定业主所属小区
        String communityId = ownerAppUserDtos.get(0).getCommunityId();
        //通过小区id查询小区信息
        CommunityDto community = new CommunityDto();
        community.setCommunityId(communityId);
        List<CommunityDto> communityDtos = communityInnerServiceSMO.queryCommunitys(community);
        //获取商户手机号
        String tel = null;
        if (communityDtos != null && communityDtos.size() > 0) {
            tel = communityDtos.get(0).getTel();
        }
        String[] roomNames = paramIn.getString("roomName").split("-");
        if (ownerAppUserDtos.size() > 0) {
            //获取openId
            String openId = ownerAppUserDtos.get(0).getOpenId();
            String url = sendMsgUrl + accessToken;
            Data data = new Data();
            PropertyFeeTemplateMessage templateMessage = new PropertyFeeTemplateMessage();
            templateMessage.setTemplate_id(templateId);
            templateMessage.setTouser(openId);
            data.setFirst(new Content("尊敬的" + paramIn.getString("personName") + "先生/女士,您好!" + communityDto.getName() + roomNames[0] + "栋" + roomNames[1] + "单元" + roomNames[2] + "室" +
                    "的装修验收申请信息如下:"));
            data.setKeyword1(new Content(paramIn.getString("stateName")));
            data.setKeyword2(new Content(paramIn.getString("remark")));
            data.setKeyword3(new Content(paramIn.getString("createTime")));
            data.setRemark(new Content("如有任何疑问,请咨询" + communityDto.getName() + "客服部,咨询电话:" + tel + "。"));
            templateMessage.setData(data);
            //获取业主公众号地址
            String wechatUrl = MappingCache.getValue("OWNER_WECHAT_URL");
            templateMessage.setUrl(wechatUrl);
            logger.info("发送模板消息内容:{}", JSON.toJSONString(templateMessage));
            ResponseEntity<String> responseEntity = outRestTemplate.postForEntity(url, JSON.toJSONString(templateMessage), String.class);
            logger.info("微信模板返回内容:{}", responseEntity);
        }
    }
}


text:
{
    'name': 'Pad on tasks',
    'version': '1.0',
    'category': 'Project Management',
    'description': """
This module adds a PAD in all project kanban views.
===================================================
    """,
    'author': 'OpenERP SA',
    'website': 'https://www.odoo.com/page/project-management',
    'depends': ['project', 'pad'],
    'data': ['project_task.xml'],
    'demo': [],
    'installable': True,
    'auto_install': True,
}
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:


text:
layout: poetry
title: The Sadness of Beauty is its Brevity
tags: poetry
---

![Sadness Of Beauty](assets/sadness-of-beauty.jpg)

The sadness of beauty is its brevity

Chromatic dances through the Milky Way

A moment captured for longevity

A blip, now here — then gone — gone astray

A punchline, summer breeze — the first sip, last bite,

A smile, the scent of attraction: red euphoria —

The winter mists and rainbows: all dissipate.

The high fives and sweet dreams; butterflies fly of phobia

A color is a shade and a blip in a void.

You've wasted your youth, neglected your ambitions.

It's drowned into grey, and you are chronically (un)employed.

Then time becomes a one-way decomposition.

But Mists are clouds for rain and eggs will spawn

Morning shower, keys in pocket, time to carry on.


text:
from __future__ import (absolute_import, division, generators, nested_scopes, print_function,
                        unicode_literals, with_statement)

import atexit
import errno
import os
import tempfile
import time
import unittest

import mock
import mox
import six

from pants.util import dirutil
from pants.util.contextutil import pushd, temporary_dir
from pants.util.dirutil import (_mkdtemp_unregister_cleaner, absolute_symlink, fast_relpath,
                                get_basedir, read_file, relative_symlink, relativize_paths, rm_rf,
                                safe_concurrent_creation, safe_file_dump, safe_mkdir, safe_mkdtemp,
                                safe_rm_oldest_items_in_dir, safe_rmtree, touch)

class DirutilTest(unittest.TestCase):

  def setUp(self):
    self._mox = mox.Mox()
    # Ensure we start in a clean state.
    _mkdtemp_unregister_cleaner()

  def tearDown(self):
    self._mox.UnsetStubs()

  def test_fast_relpath(self):
    def assertRelpath(expected, path, start):
      self.assertEquals(expected, fast_relpath(path, start))
    assertRelpath('c', '/a/b/c', '/a/b')
    assertRelpath('c', '/a/b/c', '/a/b/')
    assertRelpath('c', 'b/c', 'b')
    assertRelpath('c', 'b/c', 'b/')
    assertRelpath('c/', 'b/c/', 'b')
    assertRelpath('c/', 'b/c/', 'b/')
    assertRelpath('', 'c/', 'c/')
    assertRelpath('', 'c', 'c')
    assertRelpath('c/', 'c/', '')
    assertRelpath('c', 'c', '')

  def test_fast_relpath_invalid(self):
    with self.assertRaises(ValueError):
      fast_relpath('/a/b', '/a/baseball')
    with self.assertRaises(ValueError):
      fast_relpath('/a/baseball', '/a/b')

  def test_mkdtemp_setup_teardown(self):
    def faux_cleaner():
      pass

    DIR1, DIR2 = 'fake_dir1__does_not_exist', 'fake_dir2__does_not_exist'
    self._mox.StubOutWithMock(atexit, 'register')
    self._mox.StubOutWithMock(os, 'getpid')
    self._mox.StubOutWithMock(tempfile, 'mkdtemp')
    self._mox.StubOutWithMock(dirutil, 'safe_rmtree')
    atexit.register(faux_cleaner)  # Ensure only called once.
    tempfile.mkdtemp(dir='1').AndReturn(DIR1)
    tempfile.mkdtemp(dir='2').AndReturn(DIR2)
    os.getpid().MultipleTimes().AndReturn('unicorn')
    dirutil.safe_rmtree(DIR1)
    dirutil.safe_rmtree(DIR2)
    # Make sure other "pids" are not cleaned.
    dirutil._MKDTEMP_DIRS['fluffypants'].add('yoyo')

    try:
      self._mox.ReplayAll()
      self.assertEquals(DIR1, dirutil.safe_mkdtemp(dir='1', cleaner=faux_cleaner))
      self.assertEquals(DIR2, dirutil.safe_mkdtemp(dir='2', cleaner=faux_cleaner))
      self.assertIn('unicorn', dirutil._MKDTEMP_DIRS)
      self.assertEquals({DIR1, DIR2}, dirutil._MKDTEMP_DIRS['unicorn'])
      dirutil._mkdtemp_atexit_cleaner()
      self.assertNotIn('unicorn', dirutil._MKDTEMP_DIRS)
      self.assertEquals({'yoyo'}, dirutil._MKDTEMP_DIRS['fluffypants'])
    finally:
      dirutil._MKDTEMP_DIRS.pop('unicorn', None)
      dirutil._MKDTEMP_DIRS.pop('fluffypants', None)
      dirutil._mkdtemp_unregister_cleaner()

    self._mox.VerifyAll()

  def test_safe_walk(self):
    """Test that directory names are correctly represented as unicode strings"""
    # This test is unnecessary in python 3 since all strings are unicode there is no
    # unicode constructor.
    with temporary_dir() as tmpdir:
      safe_mkdir(os.path.join(tmpdir, '中文'))
      if isinstance(tmpdir, six.text_type):
        tmpdir = tmpdir.encode('utf-8')
      for _, dirs, _ in dirutil.safe_walk(tmpdir):
        self.assertTrue(all(isinstance(dirname, six.text_type) for dirname in dirs))

  def test_relativize_paths(self):
    build_root = '/build-root'
    jar_outside_build_root = os.path.join('/outside-build-root', 'bar.jar')
    classpath = [os.path.join(build_root, 'foo.jar'), jar_outside_build_root]
    relativized_classpath = relativize_paths(classpath, build_root)
    jar_relpath = os.path.relpath(jar_outside_build_root, build_root)
    self.assertEquals(['foo.jar', jar_relpath], relativized_classpath)

  def test_relative_symlink(self):
    with temporary_dir() as tmpdir_1:  # source and link in same dir
      source = os.path.join(tmpdir_1, 'source')
      link = os.path.join(tmpdir_1, 'link')
      rel_path = os.path.relpath(source, os.path.dirname(link))
      relative_symlink(source, link)
      self.assertTrue(os.path.islink(link))
      self.assertEquals(rel_path, os.readlink(link))

  def test_relative_symlink_source_parent(self):
    with temporary_dir() as tmpdir_1:  # source in parent dir of link
      child = os.path.join(tmpdir_1, 'child')
      os.mkdir(child)
      source = os.path.join(tmpdir_1, 'source')
      link = os.path.join(child, 'link')
      relative_symlink(source, link)
      rel_path = os.path.relpath(source, os.path.dirname(link))
      self.assertTrue(os.path.islink(link))
      self.assertEquals(rel_path, os.readlink(link))

  def test_relative_symlink_link_parent(self):
    with temporary_dir() as tmpdir_1:  # link in parent dir of source
      child = os.path.join(tmpdir_1, 'child')
      source = os.path.join(child, 'source')
      link = os.path.join(tmpdir_1, 'link')
      relative_symlink(source, link)
      rel_path = os.path.relpath(source, os.path.dirname(link))
      self.assertTrue(os.path.islink(link))
      self.assertEquals(rel_path, os.readlink(link))

  def test_relative_symlink_same_paths(self):
    with temporary_dir() as tmpdir_1:  # source is link
      source = os.path.join(tmpdir_1, 'source')
      with self.assertRaisesRegexp(ValueError, r'Path for link is identical to source'):
        relative_symlink(source, source)

  def test_relative_symlink_bad_source(self):
    with temporary_dir() as tmpdir_1:  # source is not absolute
      source = os.path.join('foo', 'bar')
      link = os.path.join(tmpdir_1, 'link')
      with self.assertRaisesRegexp(ValueError, r'Path for source.*absolute'):
        relative_symlink(source, link)

  def test_relative_symlink_bad_link(self):
    with temporary_dir() as tmpdir_1:  # link is not absolute
      source = os.path.join(tmpdir_1, 'source')
      link = os.path.join('foo', 'bar')
      with self.assertRaisesRegexp(ValueError, r'Path for link.*absolute'):
        relative_symlink(source, link)

  def test_get_basedir(self):
    self.assertEquals(get_basedir('foo/bar/baz'), 'foo')
    self.assertEquals(get_basedir('/foo/bar/baz'), '')
    self.assertEquals(get_basedir('foo'), 'foo')

  def test_rm_rf_file(self, file_name='./foo'):
    with temporary_dir() as td, pushd(td):
      touch(file_name)
      self.assertTrue(os.path.isfile(file_name))
      rm_rf(file_name)
      self.assertFalse(os.path.exists(file_name))

  def test_rm_rf_dir(self, dir_name='./bar'):
    with temporary_dir() as td, pushd(td):
      safe_mkdir(dir_name)
      self.assertTrue(os.path.isdir(dir_name))
      rm_rf(dir_name)
      self.assertFalse(os.path.exists(dir_name))

  def test_rm_rf_nonexistent(self, file_name='./non_existent_file'):
    with temporary_dir() as td, pushd(td):
      rm_rf(file_name)

  def test_rm_rf_permission_error_raises(self, file_name='./perm_guarded_file'):
    with temporary_dir() as td, pushd(td), \
         mock.patch('pants.util.dirutil.shutil.rmtree') as mock_rmtree, \
         self.assertRaises(OSError):
      mock_rmtree.side_effect = OSError(errno.EACCES, os.strerror(errno.EACCES))
      touch(file_name)
      rm_rf(file_name)

  def test_rm_rf_no_such_file_not_an_error(self, file_name='./vanishing_file'):
    with temporary_dir() as td, pushd(td), \
         mock.patch('pants.util.dirutil.shutil.rmtree') as mock_rmtree:
      mock_rmtree.side_effect = OSError(errno.ENOENT, os.strerror(errno.ENOENT))
      touch(file_name)
      rm_rf(file_name)

  def test_readwrite_file(self):
    with temporary_dir() as td:
      test_filename = os.path.join(td, 'test.out')
      test_content = '3333'
      safe_file_dump(test_filename, test_content)
      self.assertEqual(read_file(test_filename), test_content)

  def test_safe_concurrent_creation(self):
    with temporary_dir() as td:
      expected_file = os.path.join(td, 'expected_file')
      with safe_concurrent_creation(expected_file) as tmp_expected_file:
        os.mkdir(tmp_expected_file)
        self.assertTrue(os.path.exists(tmp_expected_file))
        self.assertFalse(os.path.exists(expected_file))
      self.assertTrue(os.path.exists(expected_file))

  def test_safe_concurrent_creation_noop(self):
    with temporary_dir() as td:
      expected_file = os.path.join(td, 'parent_dir', 'expected_file')

      # Ensure safe_concurrent_creation() doesn't bomb if we don't write the expected files.
      with safe_concurrent_creation(expected_file):
        pass

      self.assertFalse(os.path.exists(expected_file))
      self.assertTrue(os.path.exists(os.path.dirname(expected_file)))

  def test_safe_concurrent_creation_exception_still_renames(self):
    with temporary_dir() as td:
      expected_file = os.path.join(td, 'expected_file')

      with self.assertRaises(ZeroDivisionError):
        with safe_concurrent_creation(expected_file) as safe_path:
          os.mkdir(safe_path)
          self.assertTrue(os.path.exists(safe_path))
          raise ZeroDivisionError('zomg')

      self.assertFalse(os.path.exists(safe_path))
      self.assertTrue(os.path.exists(expected_file))

  def test_safe_rm_oldest_items_in_dir(self):
    with temporary_dir() as td:
      touch(os.path.join(td, 'file1'))
      safe_mkdir(os.path.join(td, 'file2'))
      # Time modified is only accurate to second.
      time.sleep(1.1)
      touch(os.path.join(td, 'file3'))
      touch(os.path.join(td, 'file4'))
      safe_mkdir(os.path.join(td, 'file5'))

      safe_rm_oldest_items_in_dir(td, 3)

      self.assertFalse(os.path.exists(os.path.join(td, 'file1')))
      self.assertFalse(os.path.exists(os.path.join(td, 'file2')))

      self.assertTrue(os.path.exists(os.path.join(td, 'file3')))
      self.assertTrue(os.path.exists(os.path.join(td, 'file4')))
      self.assertTrue(os.path.exists(os.path.join(td, 'file5')))

  def test_safe_rm_oldest_items_in_dir_with_excludes(self):
    with temporary_dir() as td:
      touch(os.path.join(td, 'file1'))
      touch(os.path.join(td, 'file2'))
      touch(os.path.join(td, 'file3'))
      # Time modified is only accurate to second.
      time.sleep(1.1)
      touch(os.path.join(td, 'file4'))

      excludes = [os.path.join(td, 'file1'),
                  os.path.join(td, 'file2')]
      safe_rm_oldest_items_in_dir(td, 1, excludes)

      self.assertTrue(os.path.exists(os.path.join(td, 'file1')))
      self.assertTrue(os.path.exists(os.path.join(td, 'file2')))
      self.assertTrue(os.path.exists(os.path.join(td, 'file4')))

      self.assertFalse(os.path.exists(os.path.join(td, 'file3')))

  def test_safe_rm_oldest_items_in_dir_noop(self):
    with temporary_dir() as td:
      safe_rm_oldest_items_in_dir(td, 1)
      touch(os.path.join(td, 'file1'))
      self.assertEqual(len(os.listdir(td)), 1)

class AbsoluteSymlinkTest(unittest.TestCase):
  def setUp(self):
    self.td = safe_mkdtemp()
    self.addCleanup(safe_rmtree, self.td)

    self.source = os.path.join(self.td, 'source')
    self.link = os.path.join(self.td, 'link')

  def _create_and_check_link(self, source, link):
    absolute_symlink(source, link)
    self.assertTrue(os.path.islink(link))
    self.assertEquals(source, os.readlink(link))

  def test_link(self):
    # Check if parent dirs will be created for the link
    link = os.path.join(self.td, 'a', 'b', 'c', 'self.link')
    self._create_and_check_link(self.source, link)

  def test_overwrite_link_link(self):
    # Do it twice, to make sure we can overwrite existing link
    self._create_and_check_link(self.source, self.link)
    self._create_and_check_link(self.source, self.link)

  def test_overwrite_link_file(self):
    with open(self.source, 'w') as fp:
      fp.write('evidence')

    # Do it twice, to make sure we can overwrite existing link
    self._create_and_check_link(self.source, self.link)
    self._create_and_check_link(self.source, self.link)

    # The link should have been deleted (over-written), not the file it pointed to.
    with open(self.source) as fp:
      self.assertEqual('evidence', fp.read())

  def test_overwrite_link_dir(self):
    nested_dir = os.path.join(self.source, 'a', 'b', 'c')
    os.makedirs(nested_dir)

    # Do it twice, to make sure we can overwrite existing link
    self._create_and_check_link(self.source, self.link)
    self._create_and_check_link(self.source, self.link)

    # The link should have been deleted (over-written), not the dir it pointed to.
    self.assertTrue(os.path.isdir(nested_dir))

  def test_overwrite_file(self):
    touch(self.link)
    self._create_and_check_link(self.source, self.link)

  def test_overwrite_dir(self):
    os.makedirs(os.path.join(self.link, 'a', 'b', 'c'))
    self._create_and_check_link(self.source, self.link)


text:
package com.akx2.ld32;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.math.Rectangle;
import com.badlogic.gdx.math.Vector2;

public class Player {
    final int START_HEIGHT = 420;
    final float MOVEMENT_SPEED = 420f;
    final Rectangle MOVEMENT_BOUNDS = new Rectangle(0, 100, Settings.WIDTH / 1.5f, Settings.HEIGHT);
    public final int UMBRELLAS_REQ = 5;
    public final int POWER_MAX = 3;

    final Vector2 MOUNT_POINT = new Vector2(230,25);

    Texture playerTexture;
    Texture playerDieTexture;
    TextureRegion[] playerDieRegion;

    Texture trailTexture;
    TextureRegion[] trailTextureRegions;
    Trail[] trails;
    float trailSpawnTimer = 0;
    float trailSpawnSpeed = 30;

    public Vector2 position;
    public Vector2 bounds;
    public Rectangle collisionBox;

    public int powerLevel = 0;
    public int umbrellasCollected = 0;

    public boolean isAlive = true;
    public boolean isDying = false;
    public boolean isRespawning = false;
    public int dyingAnimationIndex = 0;
    public float dyingAnimationTimer = 0;
    public float dyingAnimationSpeed = 10;

    public float respawnSafeTimer = 0;

    public int lives = 3;

    public int score = 0;

    int tI = 0;

    public Player ()
    {
        playerTexture = new Texture (Gdx.files.internal("images/player.png"), true);
        playerDieTexture = new Texture(Gdx.files.internal("images/player-die.png"), true);
        playerDieRegion = new TextureRegion[5];

        for (int i=0;i <5; i++)
        {
            playerDieRegion[i] = new TextureRegion(playerDieTexture, 0, i * 64, 256, 64);
        }

        position = new Vector2 (0, START_HEIGHT);
        bounds = new Vector2(playerTexture.getWidth(), playerTexture.getHeight());
        collisionBox = new Rectangle(0, 0, 256, 4);

        trailTexture = new Texture(Gdx.files.internal("images/needle-trail.png"), true);
        trailTextureRegions = new TextureRegion[7];
        for (tI=0;tI <7; tI++)
        {
            trailTextureRegions[tI] = new TextureRegion(trailTexture, tI * 36, 0, 36, 36);
        }
        trails = new Trail[20];
        for (tI=0; tI<trails.length; tI++)
        {
            trails[tI]=  new Trail();
        }
    }

    public void die()
    {
        if ((isAlive) && (!Settings.isHacking)) {
            isAlive = false;
            isDying = true;
            lives--;
        }
    }

    public void respawn (boolean keepWeapon)
    {
        position.set(-256, START_HEIGHT);
        isRespawning = true;
        isAlive = false;
        dyingAnimationIndex = 0;

        if (!keepWeapon)
        {
            powerLevel = 0;
            umbrellasCollected = 0;
        }
    }

    public void collectedUmbrella ()
    {
        if (isAlive) {
            umbrellasCollected++;

            if (umbrellasCollected == UMBRELLAS_REQ) {
                umbrellasCollected = 0;

                if (powerLevel < POWER_MAX) {
                    powerLevel++;
                }
            }
        }
    }

    public void render (SpriteBatch batch)
    {
        if ((isAlive) || (isRespawning))
        {
            for (tI=0; tI<trails.length; tI++)
            {
                if (trails[tI].isActive) {
                    batch.draw(trailTextureRegions[trails[tI].trailAnimationIndex], trails[tI].position.x, trails[tI].position.y + 14);
                }
            }

            if (respawnSafeTimer < 2)
            {
                batch.setColor(1, 1, 1, respawnSafeTimer % 0.5f);
                batch.draw(playerTexture, position.x, position.y);
                batch.setColor(1, 1, 1, 1);
            } else {
                batch.draw(playerTexture, position.x, position.y);
            }
        }

        if (isDying)
        {
            batch.draw(playerDieRegion[dyingAnimationIndex], position.x, position.y);
        }

    }

    public void moveLeft (float delta)
    {
        if (isAlive) {
            position.x -= MOVEMENT_SPEED * delta;
        }
    }

    public void moveRight (float delta)
    {
        if (isAlive) {
            position.x += MOVEMENT_SPEED * delta * 1.3f;
        }
    }

    public void moveUp (float delta)
    {
        if (isAlive) {
            position.y += MOVEMENT_SPEED * delta;
        }
    }

    public void moveDown (float delta)
    {
        if (isAlive) {
            position.y -= MOVEMENT_SPEED * delta;
        }
    }

    public void update (float delta)
    {
        if (isAlive) {
            // CLAMP POSITION
            if (position.x < MOVEMENT_BOUNDS.x) {
                position.x = MOVEMENT_BOUNDS.x;
            } else if (position.x + bounds.x > MOVEMENT_BOUNDS.getWidth()) {
                position.x = MOVEMENT_BOUNDS.getWidth() - bounds.x;
            }

            if (position.y < MOVEMENT_BOUNDS.y) {
                position.y = MOVEMENT_BOUNDS.y;
            } else if (position.y + bounds.y > MOVEMENT_BOUNDS.getHeight()) {
                position.y = MOVEMENT_BOUNDS.getHeight() - bounds.y;
            }

            collisionBox.x = position.x;
            collisionBox.y = position.y + 32;

            respawnSafeTimer += delta;
        }

        if (isDying)
        {
            dyingAnimationTimer += dyingAnimationSpeed * delta;
            if (dyingAnimationTimer > 1)
            {
                dyingAnimationTimer -= 1;
                dyingAnimationIndex ++;
                if (dyingAnimationIndex > 4)
                {
                    dyingAnimationIndex = 4;
                    isDying = false;
                }
            }
        }

        if (isRespawning)
        {
            position.add(500 * delta, 0);

            if (position.x > 100)
            {
                position.x = 100;
                isRespawning = false;
                isAlive = true;
                respawnSafeTimer = 0;
            }
        }

        trailSpawnTimer += trailSpawnSpeed * delta;
        if (trailSpawnTimer > 1)
        {
            trailSpawnTimer -= 1;
            spawnTrail ();
        }

        for (tI=0; tI<trails.length; tI++)
        {
            if (trails[tI].isActive) {
                trails[tI].update(delta);
            }
        }
    }

    public void spawnTrail ()
    {
        for (tI=0; tI < trails.length; tI++)
        {
            if (!trails[tI].isActive)
            {
                trails[tI].isActive = true;
                trails[tI].position.x = position.x;
                trails[tI].position.y = position.y;
                return;
            }
        }
    }

    public void dispose ()
    {
        playerTexture.dispose();
        playerDieTexture.dispose();
        trailTexture.dispose();
    }
}