#import "PluginController.h"
#import "CogPluginMulti.h"
#import "Plugin.h"

#import "Logging.h"

#import "NSFileHandle+CreateFile.h"

#import "NSDictionary+Merge.h"

#import "RedundantPlaylistDataStore.h"

#import <chrono>
#import <map>
#import <mutex>
#import <thread>

struct Cached_Metadata {
	std::chrono::steady_clock::time_point time_accessed;
	NSDictionary *properties;
	NSDictionary *metadata;
	Cached_Metadata()
	: properties(nil), metadata(nil) {
	}
};

static std::mutex Cache_Lock;

static std::map<std::string, Cached_Metadata> Cache_List;

static RedundantPlaylistDataStore *Cache_Data_Store = nil;

static bool Cache_Running = false;

static std::thread *Cache_Thread = NULL;

static void cache_run();

static void cache_init() {
	Cache_Data_Store = [[RedundantPlaylistDataStore alloc] init];
	Cache_Thread = new std::thread(cache_run);
}

static void cache_deinit() {
	Cache_Running = false;
	Cache_Thread->join();
	delete Cache_Thread;
	Cache_Data_Store = nil;
}

static void cache_insert_properties(NSURL *url, NSDictionary *properties) {
	if(properties == nil) return;

	std::lock_guard<std::mutex> lock(Cache_Lock);

	std::string path = [[url absoluteString] UTF8String];
	properties = [Cache_Data_Store coalesceEntryInfo:properties];

	Cached_Metadata &entry = Cache_List[path];

	entry.properties = properties;
	entry.time_accessed = std::chrono::steady_clock::now();
}

static void cache_insert_metadata(NSURL *url, NSDictionary *metadata) {
	if(metadata == nil) return;

	std::lock_guard<std::mutex> lock(Cache_Lock);

	std::string path = [[url absoluteString] UTF8String];
	metadata = [Cache_Data_Store coalesceEntryInfo:metadata];

	Cached_Metadata &entry = Cache_List[path];

	entry.metadata = metadata;
	entry.time_accessed = std::chrono::steady_clock::now();
}

static NSDictionary *cache_access_properties(NSURL *url) {
	std::lock_guard<std::mutex> lock(Cache_Lock);

	std::string path = [[url absoluteString] UTF8String];

	Cached_Metadata &entry = Cache_List[path];

	if(entry.properties) {
		entry.time_accessed = std::chrono::steady_clock::now();
		return entry.properties;
	}

	return nil;
}

static NSDictionary *cache_access_metadata(NSURL *url) {
	std::lock_guard<std::mutex> lock(Cache_Lock);

	std::string path = [[url absoluteString] UTF8String];

	Cached_Metadata &entry = Cache_List[path];

	if(entry.metadata) {
		entry.time_accessed = std::chrono::steady_clock::now();
		return entry.metadata;
	}

	return nil;
}

static void cache_run() {
	std::chrono::milliseconds dura(250);

	Cache_Running = true;

	while(Cache_Running) {
		std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now();

		@autoreleasepool {
			std::lock_guard<std::mutex> lock(Cache_Lock);

			size_t cacheListOriginalSize = Cache_List.size();

			for(auto it = Cache_List.begin(); it != Cache_List.end();) {
				auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(now - it->second.time_accessed);
				if(elapsed.count() >= 10) {
					it = Cache_List.erase(it);
					continue;
				}
				++it;
			}

			if(cacheListOriginalSize && Cache_List.size() == 0) {
				[Cache_Data_Store reset];
			}
		}

		std::this_thread::sleep_for(dura);
	}
}

@implementation PluginController

@synthesize sources;
@synthesize containers;
@synthesize metadataReaders;

@synthesize propertiesReadersByExtension;
@synthesize propertiesReadersByMimeType;

@synthesize decodersByExtension;
@synthesize decodersByMimeType;

@synthesize configured;

static PluginController *sharedPluginController = nil;

+ (id<CogPluginController>)sharedPluginController {
	@synchronized(self) {
		if(sharedPluginController == nil) {
			sharedPluginController = [[self alloc] init];
		}
	}

	return sharedPluginController;
}

- (id)init {
	self = [super init];
	if(self) {
		self.sources = [[NSMutableDictionary alloc] init];
		self.containers = [[NSMutableDictionary alloc] init];

		self.metadataReaders = [[NSMutableDictionary alloc] init];

		self.propertiesReadersByExtension = [[NSMutableDictionary alloc] init];
		self.propertiesReadersByMimeType = [[NSMutableDictionary alloc] init];

		self.decodersByExtension = [[NSMutableDictionary alloc] init];
		self.decodersByMimeType = [[NSMutableDictionary alloc] init];

		[self setup];

		cache_init();
	}

	return self;
}

- (void)dealloc {
	cache_deinit();
}

- (void)setup {
	if(self.configured == NO) {
		self.configured = YES;

		[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(bundleDidLoad:) name:NSBundleDidLoadNotification object:nil];

		[self loadPlugins];

		[[NSNotificationCenter defaultCenter] removeObserver:self name:NSBundleDidLoadNotification object:nil];

		[self printPluginInfo];
	}
}

- (void)bundleDidLoad:(NSNotification *)notification {
	NSArray *classNames = [[notification userInfo] objectForKey:@"NSLoadedClasses"];
	for(NSString *className in classNames) {
		Class bundleClass = NSClassFromString(className);
		if([bundleClass conformsToProtocol:@protocol(CogVersionCheck)]) {
			DLog(@"Component has version check: %@", className);
			if(![bundleClass shouldLoadForOSVersion:[[NSProcessInfo processInfo] operatingSystemVersion]]) {
				DLog(@"Plugin fails OS version check, ignoring");
				return;
			}
		}
	}
	for(NSString *className in classNames) {
		DLog(@"Class loaded: %@", className);
		Class bundleClass = NSClassFromString(className);
		if([bundleClass conformsToProtocol:@protocol(CogContainer)]) {
			[self setupContainer:className];
		}
		if([bundleClass conformsToProtocol:@protocol(CogDecoder)]) {
			[self setupDecoder:className];
		}
		if([bundleClass conformsToProtocol:@protocol(CogMetadataReader)]) {
			[self setupMetadataReader:className];
		}
		if([bundleClass conformsToProtocol:@protocol(CogPropertiesReader)]) {
			[self setupPropertiesReader:className];
		}
		if([bundleClass conformsToProtocol:@protocol(CogSource)]) {
			[self setupSource:className];
		}
	}
}

- (void)loadPluginsAtPath:(NSString *)path {
	NSArray *dirContents = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:path error:nil];

	for(NSString *pname in dirContents) {
		NSString *ppath;
		ppath = [NSString pathWithComponents:@[path, pname]];

		if([[pname pathExtension] isEqualToString:@"bundle"]) {
			NSBundle *b = [NSBundle bundleWithPath:ppath];
			[b load];
		}
	}
}

- (void)loadPlugins {
	NSArray *paths = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES);
	NSString *basePath = [[paths firstObject] stringByAppendingPathComponent:@"Cog"];

	[self loadPluginsAtPath:[[NSBundle mainBundle] builtInPlugInsPath]];
	[self loadPluginsAtPath:[basePath stringByAppendingPathComponent:@"Plugins"]];
}

- (void)setupContainer:(NSString *)className {
	Class container = NSClassFromString(className);
	if(container && [container respondsToSelector:@selector(fileTypes)]) {
		for(id fileType in [container fileTypes]) {
			NSString *ext = [fileType lowercaseString];
			NSMutableArray *containerSet;
			if(![containers objectForKey:ext]) {
				containerSet = [[NSMutableArray alloc] init];
				[containers setObject:containerSet forKey:ext];
			} else
				containerSet = [containers objectForKey:ext];
			[containerSet addObject:className];
		}
	}
}

- (void)setupDecoder:(NSString *)className {
	Class decoder = NSClassFromString(className);
	if(decoder && [decoder respondsToSelector:@selector(fileTypes)]) {
		for(id fileType in [decoder fileTypes]) {
			NSString *ext = [fileType lowercaseString];
			NSMutableArray *decoders;
			if(![decodersByExtension objectForKey:ext]) {
				decoders = [[NSMutableArray alloc] init];
				[decodersByExtension setObject:decoders forKey:ext];
			} else
				decoders = [decodersByExtension objectForKey:ext];
			[decoders addObject:className];
		}
	}

	if(decoder && [decoder respondsToSelector:@selector(mimeTypes)]) {
		for(id mimeType in [decoder mimeTypes]) {
			NSString *mimetype = [mimeType lowercaseString];
			NSMutableArray *decoders;
			if(![decodersByMimeType objectForKey:mimetype]) {
				decoders = [[NSMutableArray alloc] init];
				[decodersByMimeType setObject:decoders forKey:mimetype];
			} else
				decoders = [decodersByMimeType objectForKey:mimetype];
			[decoders addObject:className];
		}
	}
}

- (void)setupMetadataReader:(NSString *)className {
	Class metadataReader = NSClassFromString(className);
	if(metadataReader && [metadataReader respondsToSelector:@selector(fileTypes)]) {
		for(id fileType in [metadataReader fileTypes]) {
			NSString *ext = [fileType lowercaseString];
			NSMutableArray *readers;
			if(![metadataReaders objectForKey:ext]) {
				readers = [[NSMutableArray alloc] init];
				[metadataReaders setObject:readers forKey:ext];
			} else
				readers = [metadataReaders objectForKey:ext];
			[readers addObject:className];
		}
	}
}

- (void)setupPropertiesReader:(NSString *)className {
	Class propertiesReader = NSClassFromString(className);
	if(propertiesReader && [propertiesReader respondsToSelector:@selector(fileTypes)]) {
		for(id fileType in [propertiesReader fileTypes]) {
			NSString *ext = [fileType lowercaseString];
			NSMutableArray *readers;
			if(![propertiesReadersByExtension objectForKey:ext]) {
				readers = [[NSMutableArray alloc] init];
				[propertiesReadersByExtension setObject:readers forKey:ext];
			} else
				readers = [propertiesReadersByExtension objectForKey:ext];
			[readers addObject:className];
		}
	}

	if(propertiesReader && [propertiesReader respondsToSelector:@selector(mimeTypes)]) {
		for(id mimeType in [propertiesReader mimeTypes]) {
			NSString *mimetype = [mimeType lowercaseString];
			NSMutableArray *readers;
			if(![propertiesReadersByMimeType objectForKey:mimetype]) {
				readers = [[NSMutableArray alloc] init];
				[propertiesReadersByMimeType setObject:readers forKey:mimetype];
			} else
				readers = [propertiesReadersByMimeType objectForKey:mimetype];
			[readers addObject:className];
		}
	}
}

- (void)setupSource:(NSString *)className {
	Class source = NSClassFromString(className);
	if(source && [source respondsToSelector:@selector(schemes)]) {
		for(id scheme in [source schemes]) {
			[sources setObject:className forKey:scheme];
		}
	}
}

static NSString *xmlEscapeString(NSString * string) {
	CFStringRef textXML = CFXMLCreateStringByEscapingEntities(kCFAllocatorDefault, (CFStringRef)string, nil);
	if(textXML) {
		NSString *textString = (__bridge NSString *)textXML;
		CFRelease(textXML);
		return textString;
	}
	return @"";
}

- (void)printPluginInfo {
	ALog(@"Sources: %@", self.sources);
	ALog(@"Containers: %@", self.containers);
	ALog(@"Metadata Readers: %@", self.metadataReaders);

	ALog(@"Properties Readers By Extension: %@", self.propertiesReadersByExtension);
	ALog(@"Properties Readers By Mime Type: %@", self.propertiesReadersByMimeType);

	ALog(@"Decoders by Extension: %@", self.decodersByExtension);
	ALog(@"Decoders by Mime Type: %@", self.decodersByMimeType);

#if 0
    // XXX Keep in sync with Info.plist on disk!
    NSString * plistHeader = @"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\
<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n\
<plist version=\"1.0\">\n\
<dict>\n\
\t<key>FirebaseCrashlyticsCollectionEnabled</key>\n\
\t<false/>\n\
\t<key>SUEnableInstallerLauncherService</key>\n\
\t<true/>\n\
\t<key>CFBundleDevelopmentRegion</key>\n\
\t<string>en_US</string>\n\
\t<key>CFBundleDocumentTypes</key>\n\
\t<array>\n\
\t\t<dict>\n\
\t\t\t<key>CFBundleTypeExtensions</key>\n\
\t\t\t<array>\n\
\t\t\t\t<string>*</string>\n\
\t\t\t</array>\n\
\t\t\t<key>CFBundleTypeIconFile</key>\n\
\t\t\t<string>song.icns</string>\n\
\t\t\t<key>CFBundleTypeIconSystemGenerated</key>\n\
\t\t\t<integer>1</integer>\n\
\t\t\t<key>CFBundleTypeName</key>\n\
\t\t\t<string>Folder</string>\n\
\t\t\t<key>CFBundleTypeOSTypes</key>\n\
\t\t\t<array>\n\
\t\t\t\t<string>****</string>\n\
\t\t\t\t<string>fold</string>\n\
\t\t\t\t<string>disk</string>\n\
\t\t\t</array>\n\
\t\t\t<key>CFBundleTypeRole</key>\n\
\t\t\t<string>None</string>\n\
\t\t\t<key>LSHandlerRank</key>\n\
\t\t\t<string>Default</string>\n\
\t\t</dict>\n";
    NSString * plistFooter = @"\t</array>\n\
\t<key>CFBundleExecutable</key>\n\
\t<string>Cog</string>\n\
\t<key>CFBundleHelpBookName</key>\n\
\t<string>org.cogx.cog.help</string>\n\
\t<key>CFBundleIdentifier</key>\n\
\t<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>\n\
\t<key>CFBundleInfoDictionaryVersion</key>\n\
\t<string>6.0</string>\n\
\t<key>CFBundleName</key>\n\
\t<string>$(PRODUCT_NAME)</string>\n\
\t<key>CFBundleDisplayName</key>\n\
\t<string>$(PRODUCT_NAME)</string>\n\
\t<key>CFBundlePackageType</key>\n\
\t<string>APPL</string>\n\
\t<key>CFBundleShortVersionString</key>\n\
\t<string>0.08</string>\n\
\t<key>CFBundleSignature</key>\n\
\t<string>????</string>\n\
\t<key>CFBundleVersion</key>\n\
\t<string>r516</string>\n\
\t<key>LSApplicationCategoryType</key>\n\
\t<string>public.app-category.music</string>\n\
\t<key>LSMinimumSystemVersion</key>\n\
\t<string>$(MACOSX_DEPLOYMENT_TARGET)</string>\n\
\t<key>NSAppTransportSecurity</key>\n\
\t<dict>\n\
\t\t<key>NSAllowsArbitraryLoads</key>\n\
\t\t<true/>\n\
\t</dict>\n\
\t<key>NSAppleScriptEnabled</key>\n\
\t<string>YES</string>\n\
\t<key>NSCalendarsUsageDescription</key>\n\
\t<string>Cog has no use for your calendar information. Why are you trying to open your Calendar with an audio player?</string>\n\
\t<key>NSCameraUsageDescription</key>\n\
\t<string>Cog is an audio player. It will never use your camera. Why is it asking for permission to use your camera?</string>\n\
\t<key>NSContactsUsageDescription</key>\n\
\t<string>Cog has no use for your contacts information. Why are you trying to open your contacts with an audio player?</string>\n\
\t<key>NSLocationUsageDescription</key>\n\
\t<string>Cog has no use for your location information. Something is obviously wrong with the application.</string>\n\
\t<key>NSMainNibFile</key>\n\
\t<string>MainMenu</string>\n\
\t<key>NSMicrophoneUsageDescription</key>\n\
\t<string>Cog is an audio player. It does not, however, record audio. Why is it asking for permission to use your microphone?</string>\n\
\t<key>NSPhotoLibraryUsageDescription</key>\n\
\t<string>Cog is an audio player. Why are you trying to access your Photos Library with an audio player?</string>\n\
\t<key>NSPrincipalClass</key>\n\
\t<string>MediaKeysApplication</string>\n\
\t<key>NSRemindersUsageDescription</key>\n\
\t<string>Cog has no use for your reminders. Why are you trying to access them with an audio player?</string>\n\
\t<key>OSAScriptingDefinition</key>\n\
\t<string>Cog.sdef</string>\n\
\t<key>SUFeedURL</key>\n\
\t<string>https://cogcdn.cog.losno.co/mercury.xml</string>\n\
\t<key>SUPublicEDKey</key>\n\
\t<string>omxG7Rp0XK9/YEvKbVy7cd44eVAh1LJB6CmjQwjOJz4=</string>\n\
</dict>\n\
</plist>\n";
    NSMutableArray * decodersRegistered = [[NSMutableArray alloc] init];
    
    NSArray * allKeys = [self.decodersByExtension allKeys];
    for (NSString * ext in allKeys) {
        NSArray * decoders = [self.decodersByExtension objectForKey:ext];
        for (NSString * decoder in decoders) {
            if (![decodersRegistered containsObject:decoder]) {
                [decodersRegistered addObject:decoder];
            }
        }
    }

    NSMutableArray * stringList = [[NSMutableArray alloc] init];

    [stringList addObject:plistHeader];

    // These aren't handled by decoders, but as containers
    NSArray * staticTypes = @[
        @[@"M3U Playlist File", @"m3u.icns", @"m3u", @"m3u8"],
        @[@"PLS Playlist File", @"pls.icns", @"pls"],
        @[@"RAR Archive of SPC Files", @"vg.icns", @"rsn"],
        @[@"7Z Archive of VGM Files", @"vg.icns", @"vgm7z"]
    ];

    NSMutableArray * assocTypes = [[NSMutableArray alloc] init];

    [assocTypes addObjectsFromArray:staticTypes];
    
    for (NSString * decoderString in decodersRegistered) {
        Class decoder = NSClassFromString(decoderString);
        if (decoder && [decoder respondsToSelector:@selector(fileTypeAssociations)]) {
            NSArray * types = [decoder fileTypeAssociations];
            [assocTypes addObjectsFromArray:types];
        }
    }

    for (NSArray * type in assocTypes) {
        [stringList addObject:@"\t\t<dict>\n\
\t\t\t<key>CFBundleTypeExtensions</key>\n\
\t\t\t<array>\n\
"];
        for (size_t i = 2; i < [type count]; ++i) {
            [stringList addObject:@"\t\t\t\t<string>"];
            [stringList addObject:[[type objectAtIndex:i] lowercaseString]];
            [stringList addObject:@"</string>\n"];
        }
        [stringList addObject:@"\t\t\t</array>\n\
\t\t\t<key>CFBundleTypeIconFile</key>\n\
\t\t\t<string>"];
        [stringList addObject:[type objectAtIndex:1]];
        [stringList addObject:@"</string>\n\
\t\t\t<key>CFBundleTypeIconSystemGenerated</key>\n\
\t\t\t<integer>1</integer>\n\
\t\t\t<key>CFBundleTypeName</key>\n\
\t\t\t<string>"];
        [stringList addObject:xmlEscapeString([type objectAtIndex:0])];
        [stringList addObject:@"</string>\n\
\t\t\t<key>CFBundleTypeRole</key>\n\
\t\t\t<string>Viewer</string>\n\
\t\t\t<key>LSHandlerRank</key>\n\
\t\t\t<string>Default</string>\n\
\t\t\t<key>LSTypeIsPackage</key>\n\
\t\t\t<false/>\n\
\t\t</dict>\n"];
    }
    
    [stringList addObject:plistFooter];
    
    NSFileHandle *fileHandle = [NSFileHandle fileHandleForWritingAtPath:[NSTemporaryDirectory() stringByAppendingPathComponent:@"Cog_Info.plist"] createFile:YES];
    if (!fileHandle) {
        DLog(@"Error saving Info.plist!");
        return;
    }
    [fileHandle truncateFileAtOffset:0];
    [fileHandle writeData:[[stringList componentsJoinedByString:@""] dataUsingEncoding:NSUTF8StringEncoding]];
    [fileHandle closeFile];
#endif
}

- (id<CogSource>)audioSourceForURL:(NSURL *)url {
	NSString *scheme = [url scheme];

	Class source = NSClassFromString([sources objectForKey:scheme]);

	return [[source alloc] init];
}

- (NSArray *)urlsForContainerURL:(NSURL *)url {
	NSString *ext = [url pathExtension];
	NSArray *containerSet = [containers objectForKey:[ext lowercaseString]];
	NSString *classString;
	if(containerSet) {
		if([containerSet count] > 1) {
			return [CogContainerMulti urlsForContainerURL:url containers:containerSet];
		} else {
			classString = [containerSet objectAtIndex:0];
		}
	} else {
		return nil;
	}

	Class container = NSClassFromString(classString);

	return [container urlsForContainerURL:url];
}

- (NSArray *)dependencyUrlsForContainerURL:(NSURL *)url {
	NSString *ext = [url pathExtension];
	NSArray *containerSet = [containers objectForKey:[ext lowercaseString]];
	NSString *classString;
	if(containerSet) {
		if([containerSet count] > 1) {
			return [CogContainerMulti dependencyUrlsForContainerURL:url containers:containerSet];
		} else {
			classString = [containerSet objectAtIndex:0];
		}
	} else {
		return nil;
	}

	Class container = NSClassFromString(classString);

	if([container respondsToSelector:@selector(dependencyUrlsForContainerURL:)]) {
		return [container dependencyUrlsForContainerURL:url];
	} else {
		return nil;
	}
}

// Note: Source is assumed to already be opened.
- (id<CogDecoder>)audioDecoderForSource:(id<CogSource>)source skipCue:(BOOL)skip {
	NSString *ext = [[source url] pathExtension];
	NSArray *decoders = [decodersByExtension objectForKey:[ext lowercaseString]];
	NSString *classString;
	if(decoders) {
		if([decoders count] > 1) {
			if(skip) {
				NSMutableArray *_decoders = [decoders mutableCopy];
				for(int i = 0; i < [_decoders count];) {
					if([[_decoders objectAtIndex:i] isEqualToString:@"CueSheetDecoder"])
						[_decoders removeObjectAtIndex:i];
					else
						++i;
				}
				return [[CogDecoderMulti alloc] initWithDecoders:_decoders];
			}
			return [[CogDecoderMulti alloc] initWithDecoders:decoders];
		} else {
			classString = [decoders objectAtIndex:0];
		}
	} else {
		decoders = [decodersByMimeType objectForKey:[[source mimeType] lowercaseString]];
		if(decoders) {
			if([decoders count] > 1) {
				return [[CogDecoderMulti alloc] initWithDecoders:decoders];
			} else {
				classString = [decoders objectAtIndex:0];
			}
		} else {
			classString = @"SilenceDecoder";
		}
	}

	if(skip && [classString isEqualToString:@"CueSheetDecoder"]) {
		classString = @"SilenceDecoder";
	}

	Class decoder = NSClassFromString(classString);

	return [[decoder alloc] init];
}

+ (BOOL)isCoverFile:(NSString *)fileName {
	for(NSString *coverFileName in [PluginController coverNames]) {
		if([[[[fileName lastPathComponent] stringByDeletingPathExtension] lowercaseString] hasSuffix:coverFileName]) {
			return true;
		}
	}
	return false;
}

+ (NSArray *)coverNames {
	return @[@"cover", @"folder", @"album", @"front"];
}

- (NSDictionary *)metadataForURL:(NSURL *)url skipCue:(BOOL)skip {
	NSString *urlScheme = [url scheme];
	if([urlScheme isEqualToString:@"http"] ||
	   [urlScheme isEqualToString:@"https"])
		return nil;

	NSDictionary *cacheData = cache_access_metadata(url);
	if(cacheData) return cacheData;

	do {
		NSString *ext = [url pathExtension];
		NSArray *readers = [metadataReaders objectForKey:[ext lowercaseString]];
		NSString *classString;
		if(readers) {
			if([readers count] > 1) {
				if(skip) {
					NSMutableArray *_readers = [readers mutableCopy];
					for(int i = 0; i < [_readers count];) {
						if([[_readers objectAtIndex:i] isEqualToString:@"CueSheetMetadataReader"])
							[_readers removeObjectAtIndex:i];
						else
							++i;
					}
					cacheData = [CogMetadataReaderMulti metadataForURL:url readers:_readers];
					break;
				}
				cacheData = [CogMetadataReaderMulti metadataForURL:url readers:readers];
				break;
			} else {
				classString = [readers objectAtIndex:0];
			}
		} else {
			cacheData = nil;
			break;
		}

		if(skip && [classString isEqualToString:@"CueSheetMetadataReader"]) {
			cacheData = nil;
			break;
		}

		Class metadataReader = NSClassFromString(classString);

		cacheData = [metadataReader metadataForURL:url];
	} while(0);

	if(cacheData == nil) {
		cacheData = [NSDictionary dictionary];
	}

	if(cacheData) {
		NSData *image = [cacheData objectForKey:@"albumArt"];

		if(nil == image) {
			// Try to load image from external file

			NSString *path = [[url path] stringByDeletingLastPathComponent];

			// Gather list of candidate image files

			NSArray *fileNames = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:path error:nil];
			NSArray *types = @[@"jpg", @"jpeg", @"png", @"gif", @"webp", @"avif", @"heic"];
			NSArray *imageFileNames = [fileNames pathsMatchingExtensions:types];

			for(NSString *fileName in imageFileNames) {
				if([PluginController isCoverFile:fileName]) {
					image = [NSData dataWithContentsOfFile:[path stringByAppendingPathComponent:fileName]];
					break;
				}
			}

			if(image) {
				NSMutableDictionary *data = [cacheData mutableCopy];
				[data setValue:image forKey:@"albumArt"];
				cacheData = data;
			}
		}
	}

	cache_insert_metadata(url, cacheData);
	return cacheData;
}

// If no properties reader is defined, use the decoder's properties.
- (NSDictionary *)propertiesForURL:(NSURL *)url skipCue:(BOOL)skip {
	NSString *urlScheme = [url scheme];
	if([urlScheme isEqualToString:@"http"] ||
	   [urlScheme isEqualToString:@"https"])
		return nil;

	NSDictionary *properties = nil;

	properties = cache_access_properties(url);
	if(properties) return properties;

	NSString *ext = [url pathExtension];

	id<CogSource> source = [self audioSourceForURL:url];
	if(![source open:url])
		return nil;

	NSArray *readers = [propertiesReadersByExtension objectForKey:[ext lowercaseString]];
	NSString *classString = nil;
	if(readers) {
		if([readers count] > 1) {
			properties = [CogPropertiesReaderMulti propertiesForSource:source readers:readers];
			if(properties != nil && [properties count]) {
				cache_insert_properties(url, properties);
				return properties;
			}
		} else {
			classString = [readers objectAtIndex:0];
		}
	} else {
		readers = [propertiesReadersByMimeType objectForKey:[[source mimeType] lowercaseString]];
		if(readers) {
			if([readers count] > 1) {
				properties = [CogPropertiesReaderMulti propertiesForSource:source readers:readers];
				if(properties != nil && [properties count]) {
					cache_insert_properties(url, properties);
					return properties;
				}
			} else {
				classString = [readers objectAtIndex:0];
			}
		}
	}

	if(classString) {
		Class propertiesReader = NSClassFromString(classString);

		properties = [propertiesReader propertiesForSource:source];
		if(properties != nil && [properties count]) {
			cache_insert_properties(url, properties);
			return properties;
		}
	}

	{
		id<CogDecoder> decoder = [self audioDecoderForSource:source skipCue:skip];
		if(![decoder open:source]) {
			return nil;
		}

		NSDictionary *properties = [decoder properties];
		NSDictionary *metadata = [decoder metadata];

		[decoder close];

		NSDictionary *cacheData = [NSDictionary dictionaryByMerging:properties with:metadata];
		cache_insert_properties(url, cacheData);
		return cacheData;
	}
}

- (int)putMetadataInURL:(NSURL *)url {
	return 0;
}

@end