//
// File: VLPListDocument.h - Convert document from and to Cocoa plist
//
// Author(s):
//
//      (MN)    Matthias Neeracher
//
// Copyright © 2007 Matthias Neeracher
//

#import "VLPListDocument.h"
#import "VLModel.h"

//
// To convert from and to complex file formats, we use ruby scripts operating
// on the XML representation of a Cocoa property list. The property list 
// representation is strictly intended as an intermediate representation, 
// subject to change as necessary.
//

@implementation VLDocument (Plist) 

class VLPlistVisitor : public VLSongVisitor {
public:
	VLPlistVisitor(NSMutableDictionary * plist, bool performanceOrder)
		: fPlist(plist), fPerfOrder(performanceOrder) {}

	virtual void Visit(VLSong & song);
protected:
	virtual void VisitMeasure(size_t m, VLProperties & p, VLMeasure & meas);
	virtual void VisitNote(VLLyricsNote & n);
	virtual void VisitChord(VLChord & c);
	
	NSArray *		EncodeProperties(const std::vector<VLProperties> & properties);
	NSDictionary *	EncodeProperties(const VLProperties & properties);

	NSMutableDictionary *	fPlist;
	NSMutableArray *		fMeasures;
	NSMutableArray *		fNotes;
	NSMutableArray *		fChords;
	bool					fPerfOrder;
	const VLSong *			fSong;
};

NSArray * VLPlistVisitor::EncodeProperties(const std::vector<VLProperties> & properties)
{
	NSMutableArray * pa = [NSMutableArray arrayWithCapacity:properties.size()];

	for (std::vector<VLProperties>::const_iterator i = properties.begin();
		 i != properties.end(); ++i)
		[pa addObject:EncodeProperties(*i)];

	return pa;
}

NSDictionary * VLPlistVisitor::EncodeProperties(const VLProperties & properties)
{
	return [NSDictionary dictionaryWithObjectsAndKeys:
			[NSNumber numberWithInt: properties.fTime.fNum], @"timeNum",	 
			[NSNumber numberWithInt: properties.fTime.fDenom], @"timeDenom",
			[NSNumber numberWithInt: properties.fKey], @"key",
			[NSNumber numberWithInt: properties.fMode], @"mode",
			[NSNumber numberWithInt: properties.fDivisions], @"divisions",
			nil];
}

void VLPlistVisitor::Visit(VLSong & song)
{
	fSong = &song;
	fMeasures = [NSMutableArray arrayWithCapacity:32];
	VisitMeasures(song, fPerfOrder);

	[fPlist setObject:EncodeProperties(song.fProperties) forKey:@"properties"];
	[fPlist setObject:fMeasures forKey:@"measures"];
}

void VLPlistVisitor::VisitMeasure(size_t m, VLProperties & p, VLMeasure & meas)
{
	fNotes = [NSMutableArray arrayWithCapacity:1];
	fChords= [NSMutableArray arrayWithCapacity:1];
	
	VisitNotes(meas, p, true);
	VisitChords(meas);

	NSMutableDictionary * md = 
		[NSMutableDictionary dictionaryWithObjectsAndKeys:
		 [NSNumber numberWithInt:m], @"measure",
		 [NSNumber numberWithInt:meas.fPropIdx], @"properties",
		 fNotes, @"melody", fChords, @"chords",
		 nil];
	int		times;
	bool 	last;
	size_t	volta;
	if (fSong->DoesBeginRepeat(m, &times)) 
		[md setObject:
		       [NSDictionary dictionaryWithObjectsAndKeys:
				[NSNumber numberWithInt:times], @"times", nil]
			forKey: @"begin-repeat"];
	if (fSong->DoesBeginEnding(m, &last, &volta)) 
		[md setObject:
		       [NSDictionary dictionaryWithObjectsAndKeys:
				[NSNumber numberWithBool:!last], @"last",
				[NSNumber numberWithInt:volta], @"volta",
				nil]
			forKey: @"begin-ending"];
	if (fSong->DoesEndRepeat(m+1, &times)) 
		[md setObject:
		       [NSDictionary dictionaryWithObjectsAndKeys:
				[NSNumber numberWithInt:times], @"times", nil]
			forKey: @"end-repeat"];
	if (fSong->DoesEndEnding(m+1, &last, &volta)) 
		[md setObject:
		       [NSDictionary dictionaryWithObjectsAndKeys:
				[NSNumber numberWithBool:!last], @"last",
				[NSNumber numberWithInt:volta], @"volta",
				nil]
			forKey: @"end-ending"];
	if (fSong->fGoToCoda == m+1)
		[md setObject:[NSNumber numberWithBool:YES] forKey:@"tocoda"];
	if (fSong->fCoda == m)
		[md setObject:[NSNumber numberWithBool:YES] forKey:@"coda"];
	[fMeasures addObject:md];
}

void VLPlistVisitor::VisitNote(VLLyricsNote & n)
{
	NSMutableArray * ly = [NSMutableArray arrayWithCapacity:0];
	for (size_t i = 0; i<n.fLyrics.size(); ++i)
		[ly addObject:n.fLyrics[i].fText.size() 
		  ? [NSDictionary dictionaryWithObjectsAndKeys:
			 [NSString stringWithUTF8String:n.fLyrics[i].fText.c_str()], @"text",
			 [NSNumber numberWithInt:n.fLyrics[i].fKind], @"kind",
			 nil]
		  : [NSDictionary dictionary]];
		  
	NSDictionary * nd =
		[NSDictionary dictionaryWithObjectsAndKeys:
		 [NSNumber numberWithInt:n.fDuration.fNum], @"durNum",
		 [NSNumber numberWithInt:n.fDuration.fDenom], @"durDenom",
		 [NSNumber numberWithInt:n.fPitch], @"pitch",
		 [NSNumber numberWithInt:n.fTied], @"tied",
		 [NSNumber numberWithInt:n.fVisual], @"visual",
		 ly, @"lyrics",
		 nil];
	[fNotes addObject:nd];
}

void VLPlistVisitor::VisitChord(VLChord & c)
{
	NSDictionary * cd = 
		[NSDictionary dictionaryWithObjectsAndKeys:
		 [NSNumber numberWithInt:c.fDuration.fNum], @"durNum",
		 [NSNumber numberWithInt:c.fDuration.fDenom], @"durDenom",
		 [NSNumber numberWithInt:c.fPitch], @"pitch",
		 [NSNumber numberWithInt:c.fSteps], @"steps",
		 [NSNumber numberWithInt:c.fRootPitch], @"root",
		 nil];
	[fChords addObject: cd];
}

- (id)plistInPerformanceOrder:(BOOL)performanceOrder
{
	NSMutableDictionary *	plist = 
		[NSMutableDictionary dictionaryWithObjectsAndKeys:
		 songTitle, @"title", 
		 songGroove, @"groove", songTempo, @"tempo",
		 songComposer, @"composer", songLyricist, @"lyricist",
		 [NSDate date], @"saved",
		 [NSString stringWithFormat:@"VocalEasel %@",
				   [[NSBundle mainBundle] 
					   objectForInfoDictionaryKey:@"CFBundleVersion"]],
		 @"software",
		 nil];

	VLPlistVisitor	songWriter(plist, performanceOrder);
	songWriter.Visit(*song);

	return plist;
}

- (IBAction)dump:(id)sender
{	
	id plist = [self plistInPerformanceOrder:NO];
	switch ([sender tag]) {
	case 0:
		//
		// Dump as plist
		//
		NSLog(@"\n%@\n", plist);
		break;
	case 1:
		//
		// Dump as XML
		//
		plist = [[[NSString alloc] initWithData:
					 [NSPropertyListSerialization dataFromPropertyList:plist 
												  format:NSPropertyListXMLFormat_v1_0 errorDescription:nil]
								   encoding:NSUTF8StringEncoding] autorelease];
		NSLog(@"\n%@\n", plist);
		break;	
	case 2:
		//
		// Dump after roundtrip
		//
		[self readFromPlist:plist error:nil];
		plist = [self plistInPerformanceOrder:NO];
		NSLog(@"\n%@\n", plist);
		break;
	}
}

- (void)readMelody:(NSArray *)melody inMeasure:(size_t)measNo
{
	VLFraction		at(0);
	VLFraction		tiedStart(0);
	VLLyricsNote	tiedNote;

	for (NSEnumerator * ne 	  = [melody objectEnumerator];
		 NSDictionary * ndict = [ne nextObject];
	) {	
		VLLyricsNote note;
		note.fDuration = 
			VLFraction([[ndict objectForKey:@"durNum"] intValue],
					   [[ndict objectForKey:@"durDenom"] intValue],
					   true);
		note.fPitch				= [[ndict objectForKey:@"pitch"] intValue];	
		note.fTied				= 0;
		
		if ([[ndict objectForKey:@"tied"] intValue] & VLNote::kTiedWithPrev) {
			if (at != 0) {
				//
				// Extend preceding note
				//
				tiedNote.fDuration	+= note.fDuration;
				song->DelNote(measNo, tiedStart);
				song->AddNote(tiedNote, measNo, tiedStart);
				
				goto advanceAt;
			} else {
				//
				// Extend previous measure
				//
				note.fTied |= VLNote::kTiedWithPrev;
			}
		} else {
			for (NSEnumerator * le 	  = [[ndict objectForKey:@"lyrics"] objectEnumerator];
				 NSDictionary * ldict = [le nextObject];
			) {	
				VLSyllable syll;

				if (NSString * t = [ldict objectForKey:@"text"])
					syll.fText = [t UTF8String];

				syll.fKind = [[ldict objectForKey:@"kind"] intValue];

				note.fLyrics.push_back(syll);
			}
		}
		
		tiedStart	= at;
		tiedNote	= note;
		
		song->AddNote(note, measNo, at);
advanceAt:
		at += note.fDuration;		
	}
}

- (void)readChords:(NSArray *)chords inMeasure:(size_t)measNo
{
	VLFraction	at(0);

	for (NSEnumerator * ce 	  = [chords objectEnumerator];
		 NSDictionary * cdict = [ce nextObject];
	) {	
		VLChord chord;
		chord.fDuration = 
			VLFraction([[cdict objectForKey:@"durNum"] intValue],
					   [[cdict objectForKey:@"durDenom"] intValue],
					   true);
		chord.fPitch			= [[cdict objectForKey:@"pitch"] intValue];	
		chord.fRootPitch		= [[cdict objectForKey:@"root"] intValue];	
		chord.fSteps			= [[cdict objectForKey:@"steps"] intValue];	

		song->AddChord(chord, measNo, at);
		
		at += chord.fDuration;
	}
}

- (void)readMeasuresFromPlist:(NSArray *)measures
{
	std::vector<size_t>	repeatStack;

	size_t measNo = 0;
	for (NSEnumerator * me 	  = [measures objectEnumerator];
		 NSDictionary * mdict = [me nextObject];
		 ++measNo
	) {
		if (NSNumber * mNo = [mdict objectForKey:@"measure"])
			measNo = static_cast<size_t>([mNo intValue]);

		[self readMelody:[mdict objectForKey:@"melody"] inMeasure:measNo];
		[self readChords:[mdict objectForKey:@"chords"] inMeasure:measNo];

		if ([[mdict objectForKey:@"tocoda"] boolValue])
			song->fGoToCoda = measNo+1;
		if ([[mdict objectForKey:@"coda"] boolValue])
			song->fCoda = measNo;			
		if (NSDictionary * beginRep = [mdict objectForKey:@"begin-repeat"]) {
			VLRepeat 			rep;
			VLRepeat::Ending	ending(measNo, measNo, 0);

			rep.fTimes	= [[beginRep objectForKey:@"times"] intValue];
			rep.fEndings.push_back(ending);

			repeatStack.push_back(song->fRepeats.size());
			song->fRepeats.push_back(rep);
		}
		if (NSDictionary * beginEnd = [mdict objectForKey:@"begin-ending"]) {
			VLRepeat & 			rep = song->fRepeats[repeatStack.back()];
			VLRepeat::Ending	ending(measNo, measNo, 0);

			ending.fVolta	= [[beginEnd objectForKey:@"volta"] intValue];
			rep.fEndings.push_back(ending);
		}
		if (NSDictionary * endEnd = [mdict objectForKey:@"end-ending"]) {
			VLRepeat & 			rep 	= song->fRepeats[repeatStack.back()];
			VLRepeat::Ending &	ending 	= rep.fEndings.back();

			ending.fEnd = measNo+1;
			if (NSNumber * volta = [endEnd objectForKey:@"volta"])
				ending.fVolta = [volta intValue];
			while ((((1<<rep.fTimes) - 1) & ending.fVolta) < ending.fVolta)
				++rep.fTimes;
			if ([[endEnd objectForKey:@"last"] boolValue]) {
				rep.fEndings[0].fEnd = measNo+1;
				repeatStack.pop_back();
			}
		}
		if (NSDictionary * endRep = [mdict objectForKey:@"end-repeat"]) {
			VLRepeat & 			rep = song->fRepeats[repeatStack.back()];

			if (NSNumber * times = [endRep objectForKey:@"times"])
				rep.fTimes = [times intValue];

			rep.fEndings[0].fEnd 	= measNo+1;
			rep.fEndings[0].fVolta	= (1<<rep.fTimes)-1;

			repeatStack.pop_back();
		}
	}
	size_t empty = song->EmptyEnding();
	while (empty-- > 1)
		song->fMeasures.pop_back();
}

- (void)readPropertiesFromPlist:(NSArray *)properties
{
	song->fProperties.clear();
	for (NSEnumerator * pe = [properties objectEnumerator];
		 NSDictionary * pdict = [pe nextObject];
	) {
		VLProperties prop;

		prop.fTime = 
			VLFraction([[pdict objectForKey:@"timeNum"] intValue],
					   [[pdict objectForKey:@"timeDenom"] intValue],
					   false);
		prop.fKey			= [[pdict objectForKey:@"key"] intValue];
		prop.fMode			= [[pdict objectForKey:@"mode"] intValue];
		prop.fDivisions		= [[pdict objectForKey:@"divisions"] intValue];

		song->fProperties.push_back(prop);
	}
}

- (void)setValueFromPlist:(id)plist plistKey:(NSString *)plistKey forKey:(NSString *)key
{
	id value = [plist objectForKey:plistKey];
	if (value)
		[self setValue:value forKey:key];
}

- (BOOL)readFromPlist:(id)plist error:(NSError **)outError
{
	NSUndoManager * undoMgr = [self undoManager];
	[undoMgr disableUndoRegistration];
	song->clear();

	[self setValueFromPlist:plist plistKey:@"title" forKey:@"songTitle"];
	[self setValueFromPlist:plist plistKey:@"composer" forKey:@"songComposer"];
	[self setValueFromPlist:plist plistKey:@"lyricist" forKey:@"songLyricist"];
	[self setValueFromPlist:plist plistKey:@"groove" forKey:@"songGroove"];
	[self setValueFromPlist:plist plistKey:@"tempo" forKey:@"songTempo"];
	[self readPropertiesFromPlist:[plist objectForKey:@"properties"]];
	[self readMeasuresFromPlist:[plist objectForKey:@"measures"]];
	[undoMgr enableUndoRegistration];

	return YES;
}

- (NSData *)runFilter:(NSString *)filterName withContents:(NSData *)contents
{
	NSString * filterPath = [[NSBundle mainBundle] pathForResource:filterName
												   ofType:nil
												   inDirectory:@"Filters"];
	NSPipe * filterInput  = [NSPipe pipe];
	NSPipe * filterOutput = [NSPipe pipe];
	NSPipe * filterError  = [NSPipe pipe];

	NSTask * filterTask	= [[NSTask alloc] init];
	[filterTask setLaunchPath:filterPath];
	[filterTask setStandardInput:filterInput];
	[filterTask setStandardOutput:filterOutput];
	[filterTask setStandardError:filterError];
	[filterTask launch];

	NSFileHandle * inputHandle = [filterInput fileHandleForWriting];
	[inputHandle writeData:contents];
	[inputHandle closeFile];

	NSFileHandle * outputHandle = [filterOutput fileHandleForReading];
	NSData * 	   output		= [outputHandle readDataToEndOfFile];
 	
	NSFileHandle * errorHandle  = [filterError fileHandleForReading];
	NSData * 	   error		= [errorHandle readDataToEndOfFile];
 	
	[filterTask waitUntilExit];
	[filterTask release];

	if ([error length]) {
		NSString * errStr = [[[NSString alloc] initWithData:error 
			encoding:NSUTF8StringEncoding] autorelease];
		[NSException raise:NSInvalidArgumentException 
					 format:@"Filter %@: %@", filterName, errStr];
	}
	
	return output;
}

- (NSFileWrapper *)fileWrapperWithFilter:(NSString *)filterName
								   error:(NSError **)outError
{
	NSBundle * 	mainBundle = [NSBundle mainBundle];
	BOOL 	 	perfOrder  = [mainBundle pathForResource:filterName	
							  ofType:@"pwriter" inDirectory:@"Filters"] != nil;
	filterName = [filterName stringByAppendingPathExtension:
				  perfOrder ? @"pwriter" : @"writer"];
	id 		 inPlist= [self plistInPerformanceOrder:perfOrder];
	NSData * inData = 
		[NSPropertyListSerialization dataFromPropertyList:inPlist
									 format:NSPropertyListXMLFormat_v1_0 
									 errorDescription:nil];
	NSData * outData= [self runFilter:filterName withContents:inData];
	
	return [[[NSFileWrapper alloc] initRegularFileWithContents:outData]
			autorelease];
}

- (BOOL)readFromFileWrapper:(NSFileWrapper *)wrapper
				 withFilter:(NSString *)filterName
					  error:(NSError **)outError
{
	filterName = [filterName stringByAppendingPathExtension:@"reader"];

	NSData * inData 	= [wrapper regularFileContents];
	NSData * outData	= [self runFilter:filterName withContents:inData];
	NSString*errString;
	id       outPlist	= 
		[NSPropertyListSerialization propertyListFromData:outData
									 mutabilityOption:NSPropertyListImmutable
									 format:NULL errorDescription:&errString];
	if (!outPlist) 
		[NSException raise:NSInvalidArgumentException 
					 format:@"Plist %@: %@", filterName, errString];
	return [self readFromPlist:outPlist error:outError];
}

@end