//
//  MachOModel.m
//  MacDependency
//
//  Created by Konrad Windszus on 13.07.09.
//  Copyright 2009 Konrad Windszus. All rights reserved.
//

#import "MachOModel.h"
#import "MyDocument.h"

#import "ConversionStdString.h"
#import "VersionFormatter.h"
#import "ArchitectureModel.h"

#include "MachO/machoexception.h"
#include "MachO/machoheader.h"
#include "MachO/symboltablecommand.h"


// declare private methods
@interface MachOModel ()
- (void) initChildren;
- (void) checkConstraints:(DylibCommand*) dylibId;
- (void) setStateWithWarning:(BOOL)isWarning;
- (id) initWithFile:(MachO*)file command:(DylibCommand*)command document:(MyDocument*)document parent:(MachOModel*)parent architecture:(MachOArchitecture*)architecture;
@end

@implementation MachOModel

- (id) initWithFile:(MachO*)aFile document:(MyDocument*)aDocument architecture:(MachOArchitecture*)anArchitecture loadChildren:(BOOL)loadChildren {
	state = StateNormal;
	if (!(self = [self initWithFile:aFile command:nil document:aDocument parent:nil architecture:anArchitecture])) return nil;
	if (loadChildren) {
		[self initChildren];
	}
	return self;
}

// called by initChildren (calls initWithFile internally)
- (id) initWithFilename:(std::string&)filename command:(DylibCommand*)aCommand document:(MyDocument*)aDocument parent:(MachOModel*)aParent {
	BOOL isWeakReference = (command && !command->isNecessary());
    MachO* aFile;
    MachOArchitecture* anArchitecture;
    state = StateNormal;
	try {
		aFile = [aDocument cache]->getFile(filename, aParent->file); // throws exception in case file is not found
		anArchitecture = aFile->getCompatibleArchitecture(aParent->architecture);
		if (!anArchitecture) {
			[self setStateWithWarning:isWeakReference];
			NSString* log = [NSString stringWithFormat:NSLocalizedString(@"ERR_ARCHITECTURE_MISMATCH", nil), filename.c_str(), [parent name]];
			[aDocument appendLogLine:log withModel:self state:state];
		}
		
	}  catch (MachOException& exc) {
		[self setStateWithWarning:isWeakReference];
		NSString* log = [NSString stringWithStdString:exc.getCause()];
		[aDocument appendLogLine:log withModel:self state:state];
		// distinguish between weak and strong. In both cases append to tree with a status color				
	}
	[aDocument incrementNumDependencies];
	if (!(self = [self initWithFile:aFile command:aCommand document:aDocument parent:aParent architecture:anArchitecture])) return nil;
	return self;
}				 

// private
- (id) initWithFile:(MachO*)aFile command:(DylibCommand*)aCommand document:(MyDocument*)aDocument parent:(MachOModel*)aParent architecture:(MachOArchitecture*)anArchitecture {
	if (self = [super init]) {
		document = aDocument;
		parent = aParent;
		command = aCommand;
		file = aFile;
		children = nil;
		architecture = anArchitecture;
		if (architecture) {
			DylibCommand* dylibId = architecture->getDynamicLibIdCommand();
			if (dylibId && command) {
				[self checkConstraints:dylibId];
			}
		} 
		
	} return self;
}


- (void) setStateWithWarning:(BOOL)isWarning {
	State newState = StateError;
	if (isWarning) {
		newState = StateWarning;
	}
	
	if (self->state < newState) {
		self->state = newState;
	}
}

- (id) parent {
	return parent;
}

- (void) checkConstraints:(DylibCommand*) dylibId {
	// check version information (only compatible version information is relevant)
	unsigned int minVersion = dylibId->getCompatibleVersion();
	unsigned int requestedMinVersion = command->getCompatibleVersion();
	unsigned int requestedMaxVersion = command->getCurrentVersion();
	
	VersionFormatter* versionFormatter = [[VersionFormatter alloc] init];
	
	BOOL isWeakReference = (command && !command->isNecessary());
	
	NSString* log;
	// check minimum version
	if (minVersion != 0 && requestedMinVersion != 0 && minVersion < requestedMinVersion) {
		[self setStateWithWarning:isWeakReference];
		log = [NSString stringWithFormat:NSLocalizedString(@"ERR_MINIMUM_VERSION", nil), parent->command->getName().c_str(), [parent name], [versionFormatter stringForObjectValue:[NSNumber numberWithUnsignedInt:requestedMinVersion]], [versionFormatter stringForObjectValue:[NSNumber numberWithUnsignedInt:minVersion]]];
		[document appendLogLine:log withModel:self state:state];
		
	}
	
	// extended checks which are currently not done by dyld
	
	// check maximum version
	if (minVersion != 0 && requestedMaxVersion != 0 && minVersion > requestedMaxVersion) {
		[self setStateWithWarning:YES];
		log = [NSString stringWithFormat:NSLocalizedString(@"ERR_MAXIMUM_VERSION", nil), command->getName().c_str(), [parent name], [versionFormatter stringForObjectValue:[NSNumber numberWithUnsignedInt:requestedMaxVersion]], [versionFormatter stringForObjectValue:[NSNumber numberWithUnsignedInt:minVersion]]];
		[document appendLogLine:log withModel:self state:state];
		
	}
	
	// check names
	if (dylibId->getName() != command->getName()) {
		[self setStateWithWarning:YES];
		log = [NSString stringWithFormat:NSLocalizedString(@"ERR_NAME_MISMATCH", nil), command->getName().c_str(), [parent name], dylibId->getName().c_str()];
		[document appendLogLine:log withModel:self state:state];
	}
	
	// TODO: check for relative paths, since they can lead to problems
} 

- (NSArray*)children {
	if (children == nil) {
		[self initChildren];
	}
	return children;
}

- (void) initChildren {
	// TODO: tweak capacity
	children = [NSMutableArray arrayWithCapacity:20];
	if (architecture) {
		std::string workingDirectory = [[document workingDirectory] stdString];
		for (MachOArchitecture::LoadCommandsConstIterator it = architecture->getLoadCommandsBegin();
			 it != architecture->getLoadCommandsEnd();
			 ++it) {
			
			LoadCommand* childLoadCommand = (*it);
			// check if it is dylibcommand
			DylibCommand* dylibCommand = dynamic_cast<DylibCommand*> (childLoadCommand);
			if (dylibCommand != NULL && !dylibCommand->isId()) {
				std::string filename = architecture->getResolvedName(dylibCommand->getName(), workingDirectory);

        [children addObject: [[MachOModel alloc] initWithFilename: filename command: dylibCommand document: document parent: self]];
			}
		}
	}
}

- (BOOL)isLeaf {
	[self children];
	if ([children count] == 0)
		return YES;
	return NO;
}

- (NSColor*) textColor {
	NSColor* color;
	switch(state) {
		case StateWarning:
			color = [NSColor blueColor];
			break;
		case StateError:
			color = [NSColor redColor];
			break;
		default:
			color= [NSColor blackColor];
	}
	return color;
}

- (NSString*) filename {
	std::string filename;
	if (file)
		filename = file->getFileName();
	return [NSString stringWithStdString:filename];
}

- (NSString*) version {
	if (file) {
		return [NSString stringWithStdString:file->getVersion()];
	}
	return @"";
}

- (NSNumber*) size {
  if (file != nullptr)
    return @(file->getSize());
  return @(0);
}

- (NSString*) name {
	std::string name;
	if (command) {
		name = command->getName();
	} else if (file) {
		name = file->getName();
	}
	return [NSString stringWithStdString:name];
}

- (NSNumber*) currentVersion {
	if (command) {
		return [NSNumber numberWithUnsignedInt:command->getCurrentVersion()];
	}
	return [NSNumber numberWithUnsignedInt:0];
}

- (NSNumber*) compatibleVersion {
	if (command) {
		return [NSNumber numberWithUnsignedInt:command->getCompatibleVersion()];
	}
	return [NSNumber numberWithUnsignedInt:0];
}

- (NSDate*) lastModificationTime {
	if (file)
		return [NSDate dateWithTimeIntervalSince1970:file->getLastModificationTime()];
	return NULL;
}

- (NSString*) dependencyType {
	NSString* type;
	if (!command) {
		return @"";
	}
	switch(command->getType()) {
			
		case DylibCommand::DependencyWeak:		
			type = NSLocalizedString(@"DEPENDENCY_TYPE_WEAK", nil);
			break;
		case DylibCommand::DependencyDelayed:   
			type = NSLocalizedString(@"DEPENDENCY_TYPE_DELAYED", nil);
			break;
		case DylibCommand::DependencyNormal:  		
			type = NSLocalizedString(@"DEPENDENCY_TYPE_NORMAL", nil);
			break;
		default:
			type = NSLocalizedString(@"UNDEFINED", @"Unknown");
	}
	return type;
}

- (NSArray*) architectures {
	NSMutableArray* architectures = [NSMutableArray arrayWithCapacity:4];
	
	if (file) {
		for (MachO::MachOArchitecturesIterator iter = file->getArchitecturesBegin(); iter != file->getArchitecturesEnd(); iter++) {
			// create model for architecture
			ArchitectureModel* currentArchitecture = [[ArchitectureModel alloc]initWithArchitecture:(*iter) file:file document:document isRoot:NO];
			// correct order (current architecture should have first index)
			if ((*iter) == architecture) {
				[architectures insertObject:currentArchitecture atIndex:0]; // insert at beginning
			} else {
				[architectures addObject:currentArchitecture]; // insert at end
			}
			
		}
	}
	return  architectures;
}
@end
