//-----------------------------------------------------------------------------
// Project     : VST SDK
//
// Category    : Helpers
// Filename    : public.sdk/source/vst/hosting/module_mac.mm
// Created by  : Steinberg, 08/2016
// Description : hosting module classes (macOS implementation)
//
//-----------------------------------------------------------------------------
// LICENSE
// (c) 2023, Steinberg Media Technologies GmbH, All Rights Reserved
//-----------------------------------------------------------------------------
// Redistribution and use in source and binary forms, with or without modification,
// are permitted provided that the following conditions are met:
//
//   * Redistributions of source code must retain the above copyright notice,
//     this list of conditions and the following disclaimer.
//   * Redistributions in binary form must reproduce the above copyright notice,
//     this list of conditions and the following disclaimer in the documentation
//     and/or other materials provided with the distribution.
//   * Neither the name of the Steinberg Media Technologies nor the names of its
//     contributors may be used to endorse or promote products derived from this
//     software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
// IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
// INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
// BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
// OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE  OF THIS SOFTWARE, EVEN IF ADVISED
// OF THE POSSIBILITY OF SUCH DAMAGE.
//-----------------------------------------------------------------------------

#import "module.h"

#import <Cocoa/Cocoa.h>
#import <CoreFoundation/CoreFoundation.h>

#if !__has_feature(objc_arc)
#error this file needs to be compiled with automatic reference counting enabled
#endif

//------------------------------------------------------------------------
extern "C" {
typedef bool (*BundleEntryFunc) (CFBundleRef);
typedef bool (*BundleExitFunc) ();
}

//------------------------------------------------------------------------
namespace VST3 {
namespace Hosting {

//------------------------------------------------------------------------
namespace {

//------------------------------------------------------------------------
template <typename T>
class CFPtr
{
public:
	inline CFPtr (const T& obj = nullptr) : obj (obj) {}
	inline CFPtr (CFPtr&& other) { *this = other; }
	inline ~CFPtr ()
	{
		if (obj)
			CFRelease (obj);
	}

	inline CFPtr& operator= (CFPtr&& other)
	{
		obj = other.obj;
		other.obj = nullptr;
		return *this;
	}
	inline CFPtr& operator= (const T& o)
	{
		if (obj)
			CFRelease (obj);
		obj = o;
		return *this;
	}
	inline operator T () const { return obj; } // act as T
private:
	CFPtr (const CFPtr& other) = delete;
	CFPtr& operator= (const CFPtr& other) = delete;

	T obj = nullptr;
};

//------------------------------------------------------------------------
class MacModule : public Module
{
public:
	template <typename T>
	T getFunctionPointer (const char* name)
	{
		assert (bundle);
		CFPtr<CFStringRef> functionName (
		    CFStringCreateWithCString (kCFAllocatorDefault, name, kCFStringEncodingASCII));
		return reinterpret_cast<T> (CFBundleGetFunctionPointerForName (bundle, functionName));
	}

	bool loadInternal (const std::string& path, std::string& errorDescription)
	{
		CFPtr<CFURLRef> url (CFURLCreateFromFileSystemRepresentation (
		    kCFAllocatorDefault, reinterpret_cast<const UInt8*> (path.data ()), path.length (),
		    true));
		if (!url)
			return false;
		bundle = CFBundleCreate (kCFAllocatorDefault, url);
		CFErrorRef error = nullptr;
		if (!bundle || !CFBundleLoadExecutableAndReturnError (bundle, &error))
		{
			if (error)
			{
				CFPtr<CFStringRef> errorString (CFErrorCopyDescription (error));
				if (errorString)
				{
					auto stringLength = CFStringGetLength (errorString);
					auto maxSize =
					    CFStringGetMaximumSizeForEncoding (stringLength, kCFStringEncodingUTF8);
					auto buffer = std::make_unique<char[]> (maxSize);
					if (CFStringGetCString (errorString, buffer.get (), maxSize,
					                        kCFStringEncodingUTF8))
						errorDescription = buffer.get ();
					CFRelease (error);
				}
			}
			else
			{
				errorDescription = "Could not create Bundle for path: " + path;
			}
			return false;
		}
		// bundleEntry is mandatory
		auto bundleEntry = getFunctionPointer<BundleEntryFunc> ("bundleEntry");
		if (!bundleEntry)
		{
			errorDescription = "Bundle does not export the required 'bundleEntry' function";
			return false;
		}
		// bundleExit is mandatory
		auto bundleExit = getFunctionPointer<BundleExitFunc> ("bundleExit");
		if (!bundleExit)
		{
			errorDescription = "Bundle does not export the required 'bundleExit' function";
			return false;
		}
		auto factoryProc = getFunctionPointer<GetFactoryProc> ("GetPluginFactory");
		if (!factoryProc)
		{
			errorDescription = "Bundle does not export the required 'GetPluginFactory' function";
			return false;
		}
		if (!bundleEntry (bundle))
		{
			errorDescription = "Calling 'bundleEntry' failed";
			return false;
		}
		auto f = owned (factoryProc ());
		if (!f)
		{
			errorDescription = "Calling 'GetPluginFactory' returned nullptr";
			return false;
		}
		factory = PluginFactory (f);
		return true;
	}

	bool load (const std::string& path, std::string& errorDescription) override
	{
		if (!path.empty () && path[0] != '/')
		{
			auto buffer = std::make_unique<char[]> (PATH_MAX);
			auto workDir = getcwd (buffer.get (), PATH_MAX);
			if (workDir)
			{
				std::string wd (workDir);
				wd += "/";
				return loadInternal (wd + path, errorDescription);
			}
		}
		return loadInternal (path, errorDescription);
	}

	~MacModule () override
	{
		factory = PluginFactory (nullptr);

		if (bundle)
		{
			if (auto bundleExit = getFunctionPointer<BundleExitFunc> ("bundleExit"))
				bundleExit ();
		}
	}

	CFPtr<CFBundleRef> bundle;
};

//------------------------------------------------------------------------
void findModulesInDirectory (NSURL* dirUrl, Module::PathList& result)
{
	dirUrl = [dirUrl URLByResolvingSymlinksInPath];
	if (!dirUrl)
		return;
	NSDirectoryEnumerator* enumerator = [[NSFileManager defaultManager]
	               enumeratorAtURL: dirUrl
	    includingPropertiesForKeys:nil
	                       options:NSDirectoryEnumerationSkipsPackageDescendants
	                  errorHandler:nil];
	for (NSURL* url in enumerator)
	{
		if ([[[url lastPathComponent] pathExtension] isEqualToString:@"vst3"])
		{
			CFPtr<CFArrayRef> archs (
			    CFBundleCopyExecutableArchitecturesForURL (static_cast<CFURLRef> (url)));
			if (archs)
				result.emplace_back ([url.path UTF8String]);
		}
		else
		{
			id resValue;
			if (![url getResourceValue:&resValue forKey:NSURLIsSymbolicLinkKey error:nil])
				continue;
			if (!static_cast<NSNumber*> (resValue).boolValue)
				continue;
			auto resolvedUrl = [url URLByResolvingSymlinksInPath];
			if (![resolvedUrl getResourceValue:&resValue forKey:NSURLIsDirectoryKey error:nil])
				continue;
			if (!static_cast<NSNumber*> (resValue).boolValue)
				continue;
			findModulesInDirectory (resolvedUrl, result);
		}
	}
}

//------------------------------------------------------------------------
void getModules (NSSearchPathDomainMask domain, Module::PathList& result)
{
	NSURL* libraryUrl = [[NSFileManager defaultManager] URLForDirectory:NSLibraryDirectory
	                                                           inDomain:domain
	                                                  appropriateForURL:nil
	                                                             create:NO
	                                                              error:nil];
	if (libraryUrl == nil)
		return;
	NSURL* audioUrl = [libraryUrl URLByAppendingPathComponent:@"Audio"];
	if (audioUrl == nil)
		return;
	NSURL* plugInsUrl = [audioUrl URLByAppendingPathComponent:@"Plug-Ins"];
	if (plugInsUrl == nil)
		return;
	NSURL* vst3Url =
	    [[plugInsUrl URLByAppendingPathComponent:@"VST3"] URLByResolvingSymlinksInPath];
	if (vst3Url == nil)
		return;
	findModulesInDirectory (vst3Url, result);
}

//------------------------------------------------------------------------
void getApplicationModules (Module::PathList& result)
{
	auto bundle = CFBundleGetMainBundle ();
	if (!bundle)
		return;
	auto bundleUrl = static_cast<NSURL*> (CFBridgingRelease (CFBundleCopyBundleURL (bundle)));
	if (!bundleUrl)
		return;
	auto resUrl = [bundleUrl URLByAppendingPathComponent:@"Contents"];
	if (!resUrl)
		return;
	auto vst3Url = [resUrl URLByAppendingPathComponent:@"VST3"];
	if (!vst3Url)
		return;
	findModulesInDirectory (vst3Url, result);
}

//------------------------------------------------------------------------
void getModuleSnapshots (const std::string& path, Module::SnapshotList& result)
{
	auto nsString = [NSString stringWithUTF8String:path.data ()];
	if (!nsString)
		return;
	auto bundleUrl = [NSURL fileURLWithPath:nsString];
	if (!bundleUrl)
		return;
	auto urls = [NSBundle URLsForResourcesWithExtension:@"png"
	                                       subdirectory:@"Snapshots"
	                                    inBundleWithURL:bundleUrl];
	if (!urls || [urls count] == 0)
		return;

	for (NSURL* url in urls)
	{
		std::string fullpath ([[url path] UTF8String]);
		std::string filename ([[[url path] lastPathComponent] UTF8String]);
		auto uid = Module::Snapshot::decodeUID (filename);
		if (!uid)
			continue;

		auto scaleFactor = 1.;
		if (auto decodedScaleFactor = Module::Snapshot::decodeScaleFactor (filename))
			scaleFactor = *decodedScaleFactor;

		Module::Snapshot::ImageDesc desc;
		desc.scaleFactor = scaleFactor;
		desc.path = std::move (fullpath);
		bool found = false;
		for (auto& entry : result)
		{
			if (entry.uid != *uid)
				continue;
			found = true;
			entry.images.emplace_back (std::move (desc));
			break;
		}
		if (found)
			continue;
		Module::Snapshot snapshot;
		snapshot.uid = *uid;
		snapshot.images.emplace_back (std::move (desc));
		result.emplace_back (std::move (snapshot));
	}
}

//------------------------------------------------------------------------
} // anonymous

//------------------------------------------------------------------------
Module::Ptr Module::create (const std::string& path, std::string& errorDescription)
{
	auto module = std::make_shared<MacModule> ();
	if (module->load (path, errorDescription))
	{
		module->path = path;
		auto it = std::find_if (path.rbegin (), path.rend (),
		                        [] (const std::string::value_type& c) { return c == '/'; });
		if (it != path.rend ())
			module->name = {it.base (), path.end ()};
		return std::move (module);
	}
	return nullptr;
}

//------------------------------------------------------------------------
Module::PathList Module::getModulePaths ()
{
	PathList list;
	getModules (NSUserDomainMask, list);
	getModules (NSLocalDomainMask, list);
	// TODO getModules (NSNetworkDomainMask, list);
	getApplicationModules (list);
	return list;
}

//------------------------------------------------------------------------
Module::SnapshotList Module::getSnapshots (const std::string& modulePath)
{
	SnapshotList list;
	getModuleSnapshots (modulePath, list);
	return list;
}

//------------------------------------------------------------------------
Optional<std::string> Module::getModuleInfoPath (const std::string& modulePath)
{
	auto nsString = [NSString stringWithUTF8String:modulePath.data ()];
	if (!nsString)
		return {};
	auto bundleUrl = [NSURL fileURLWithPath:nsString];
	if (!bundleUrl)
		return {};
	auto moduleInfoUrl = [NSBundle URLForResource:@"moduleinfo"
	                                withExtension:@"json"
	                                 subdirectory:nullptr
	                              inBundleWithURL:bundleUrl];
	if (!moduleInfoUrl)
		return {};
	NSError* error = nil;
	if ([moduleInfoUrl checkResourceIsReachableAndReturnError:&error])
		return {std::string (moduleInfoUrl.fileSystemRepresentation)};
	return {};
}

//------------------------------------------------------------------------
} // Hosting
} // VST3
