#pragma warning(push, 0)
#pragma warning(disable: 4146 4996)
#include <llvm/ExecutionEngine/Orc/JITTargetMachineBuilder.h>
#include <llvm/ExecutionEngine/Orc/LLJIT.h>
#include <llvm/ExecutionEngine/Orc/RTDyldObjectLinkingLayer.h>
#include <llvm/ExecutionEngine/RTDyldMemoryManager.h>
#include <llvm/ExecutionEngine/SectionMemoryManager.h>
#include <llvm/ExecutionEngine/Orc/ObjectLinkingLayer.h>
#include <llvm/ExecutionEngine/Orc/ThreadSafeModule.h>
#include <llvm/IR/LLVMContext.h>
#include <llvm/IR/Module.h>
#include <llvm/IRReader/IRReader.h>
#include <llvm/Support/SourceMgr.h>
#include <llvm/Support/TargetSelect.h>
#pragma warning(pop)

/*
#1 Why did the main CRT lib not work?
#2 No idea where to get an ObjectLinkingLayer from :/
*/

namespace JIT
{
bool                     addModule(const char *name, const char *file);
uintptr_t                getAddress(const char *name, const char *function);
llvm::JITEvaluatedSymbol lookupResolved(const char *name, const char *function);

class MyTentativeDefinitionMaterializationUnit : public llvm::orc::MaterializationUnit 
{
private:
	//#2 llvm::StringRef               ModuleName;
	//#2 llvm::orc::ObjectLinkingLayer &LinkLayer;

	public:

		// Constructor tells us the name of the module (e.g. Planschi) that
		// we want to try to load, and the ObjectLinkingLayer that we will
		// use to load it.
		MyTentativeDefinitionMaterializationUnit(/*llvm::StringRef ModuleName, #2 llvm::orc::ObjectLinkingLayer &LinkLayer,*/ llvm::orc::SymbolFlagsMap InitalSymbolFlags, llvm::orc::SymbolStringPtr InitSymbol, llvm::orc::VModuleKey K) 
			: MaterializationUnit(std::move(InitalSymbolFlags), std::move(InitSymbol), K)
			  //#2 ModuleName(ModuleName), LinkLayer(LinkLayer) 
		{}

		// getName is used in JIT debug logging.
		virtual llvm::StringRef getName() const override 
		{
			return "MyTentativeDefinitionMaterializationUnit";
		}

		// Our materialize method will be called by the JIT outside the session lock
		// to materialize the symbols contained in R->getSymbols() (which is the same
		// set of symbols passed in to the constructor as InitialSymbolFlags).
		virtual void materialize(llvm::orc::MaterializationResponsibility R) override 
		{
			printf("DINO!\n");

			llvm::dbgs() << "I am a tentative definition unit trying to materialize "
				"the following symbols for module " << "Wuff?" << ": "
				<< R.getSymbols() << "\n";

			if(R.getSymbols().size() > 0ull)
			{
				if(strcmp((*((*R.getSymbols().begin()).first)).data(), "?_Plansch_test@@3HA") == 0)
				{
					if(addModule("Plansch", "CM_Plansch.bc"))
					{
						llvm::orc::SymbolFlagsMap NewSymbols;

						auto adr = lookupResolved("Plansch", "?test@@3HC");
						if(adr)
						{
							//And now? :/
							NewSymbols.insert(std::make_pair(nullptr, adr.getFlags()));
						}

						R.defineMaterializing(std::move(NewSymbols));
						return;
					}

					else
					{
						printf("Badewanne...\n");
					}
				}
			}

			R.failMaterialization();

			// Try to produce an object file for ModuleName. This is where all the work
			// specific to your use-case will happen.
			//llvm::Expected<std::unique_ptr<llvm::MemoryBuffer>> ObjectForModule = requestObjectForModule(ModuleName);

			// If we didn't get an object file then register the failure and bail out.
			/*if (!ObjectForModule) {
				LinkLayer.getExecutionSession().reportError(ObjectForModule.takeError());
				R->failMaterialization();
				return.
			}*/

			// If we did get an object file back then we need to check the set of
			// definitions that it provided: There might be more than just the symbols
			// we were searching for (e.g. if module Planschi defines "test" and "foo"
			// and we only created this tentative unit to cover "test" then we need to
			// tell the JIT that we're taking responsibility for "foo" too.
			/*auto ProvidedSymbols = getObjectSymbolInfo(LinkLayer.getExecutionSession(),
				*ObjectForModule);*/

			// If there was something wrong with the object and we couldn't get the set
			// of symbols that it defines then bail out.
			/*if (!ProvidedSymbols) {
				LinkLayer.getExecutionSession().reportError(ProvidedSymbols.takeError());
				R->failMaterialization();
				return;
			}*/

			// Otherwise we can define the set of new symbols as the set of all defined
			// symbols minus the ones we already knew about:
			/*SymbolFlagsMap NewSymbols = std::move(ProvidedSymbols->first);
			for (auto &KV : R->getSymbols())
				NewSymbols.erase(KV.first);*/

			// If the set of new symbols is non-empty then notify the JIT about the
			// new symbols.
			/*if (!NewSymbols.empty()) {
				if (auto Err = R->defineMaterializing(std::move(NewSymbols))) {
					// If there was an error, for example one of our new symbols clashed
					// with an existing definition, then bail out.
					LinkLayer.getExecutionSession().reportError(std::move(Err));
					R->failMaterialization();
					return;
				}
			}*/

			// Otherwise we're all done: hand the object off to be linked.
			//LinkLayer.emit(std::move(R), std::move(*ObjectForModule));
		}

		virtual void discard(const llvm::orc::JITDylib &JD, const llvm::orc::SymbolStringPtr &Name) override
		{}
	};


class DefinitionGenerator : public llvm::orc::JITDylib::DefinitionGenerator
{
public:
	DefinitionGenerator() = default;

	virtual llvm::Error tryToGenerate(llvm::orc::LookupKind K, llvm::orc::JITDylib &JD, llvm::orc::JITDylibLookupFlags JDLookupFlags, const llvm::orc::SymbolLookupSet &LookupSet) override
	{
		llvm::orc::SymbolFlagsMap notFound;
		llvm::orc::SymbolMap      NewSymbols;

		for(const auto &name : LookupSet)
		{
			const uintptr_t adr = 0x0;
			if(adr) 
			{
				NewSymbols[name.first] = llvm::JITEvaluatedSymbol(adr, llvm::JITSymbolFlags::Absolute);
			}

			else
			{
				printf("Didn't found %s\n", (*name.first).data());

				//MyTentativeDefinitionMaterializationUnit wuf(

				notFound.insert(std::make_pair(name.first, llvm::JITSymbolFlags::Absolute));
			}
		}		

		JD.define(absoluteSymbols(std::move(NewSymbols)));
		if(notFound.size() > 0)
		{
			JD.define(std::make_unique<MyTentativeDefinitionMaterializationUnit>(std::move(notFound), nullptr, 0));
		}

		return llvm::Error::success();
	}
};

static std::unique_ptr<llvm::orc::LLJIT> LLJIT;

static void printLLVMError(const llvm::ErrorInfoBase &M) 
{
	//printf("%s", M.message().c_str());
}

static void setupLLVM()
{
	llvm::InitializeNativeTarget();
	llvm::InitializeNativeTargetAsmPrinter();
	llvm::InitializeNativeTargetAsmParser();
}

static llvm::Expected<llvm::orc::JITTargetMachineBuilder> createTargetMachineBuilder()
{
	auto JTMB = llvm::orc::JITTargetMachineBuilder::detectHost();
	if(JTMB)
	{
		//Nothing to configurate
	}

	return JTMB;
}

static std::unique_ptr<llvm::orc::LLJIT> createJIT()
{
	auto JTMB = createTargetMachineBuilder();
	if(JTMB)
	{
		llvm::orc::LLJITBuilder jitBuilder;

		jitBuilder.setJITTargetMachineBuilder(std::move(JTMB.get()));
		auto tmpJIT = jitBuilder.create();
		if(tmpJIT)
		{
			return std::move(tmpJIT.get());
		}
	}

	return nullptr;
}

static llvm::orc::ThreadSafeModule loadModule(const char *path)
{
	std::unique_ptr<llvm::LLVMContext> ctx = std::make_unique<llvm::LLVMContext>();
	llvm::SMDiagnostic diagnostic;

	auto module = llvm::parseIRFile(path, diagnostic, *ctx);
	if(module)
	{
		//Grab Symbols of interesst here...
		return llvm::orc::ThreadSafeModule(std::move(module), std::move(ctx));
	}

	return llvm::orc::ThreadSafeModule();
}

static void defineCRTFunctions(llvm::orc::JITDylib &lib)
{
	llvm::orc::SymbolMap cLib;
	auto &ES  = LLJIT->getExecutionSession();

#define __makeSymbol(name, adr) \
cLib[ES.intern(name)] = llvm::JITEvaluatedSymbol((uintptr_t)adr, llvm::JITSymbolFlags::Absolute)

	__makeSymbol("__acrt_iob_func",         &__acrt_iob_func);
	__makeSymbol("__stdio_common_vfprintf", &__stdio_common_vfprintf);

#undef __makeSymbol

	lib.define(absoluteSymbols(std::move(cLib)));
}

bool setup()
{
	setupLLVM();
	LLJIT = createJIT();

	if(LLJIT)
	{
		//Define some standard stuff
		//defineCRTFunctions(LLJIT->getMainJITDylib()); #1
		return true;
	}

	return false;
}

bool addModule(const char *name, const char *file)
{
	auto module = loadModule(file);
	if(module)
	{
		//We could grab symbols of interresset here too!
		auto dyLib = LLJIT->createJITDylib(name);
		if(dyLib)
		{
			auto &curLib = dyLib.get();

			curLib.addGenerator(std::make_unique<DefinitionGenerator>());
			//curLib.define(std::make_unique<MyTentativeDefinitionMaterializationUnit>());

			//#1
			defineCRTFunctions(curLib);
			//curLib.addToLinkOrder(LLJIT->getMainJITDylib()); #1

			auto result = LLJIT->addIRModule(curLib, std::move(module));
			return !result; //False means everything is fine?!
		}
	}

	return false;
}

uintptr_t getAddress(const char *name, const char *function)
{
	auto *dyLib = LLJIT->getJITDylibByName(name);
	if(dyLib)
	{
		auto result = LLJIT->lookup(*dyLib, function);
		if(result)
		{
			return result->getAddress();
		}

		else
		{
			//Print error code here
			llvm::handleAllErrors(result.takeError(), printLLVMError);
		}
	}

	return 0x0;
}

llvm::JITEvaluatedSymbol lookupResolved(const char *name, const char *function)
{
	auto *dyLib = LLJIT->getJITDylibByName(name);

	if(dyLib)
	{
		auto result = LLJIT->getExecutionSession().lookup({dyLib}, function, llvm::orc::SymbolState::Resolved);
		if(result)
		{
			return result.get();
		}
	}
	return llvm::JITEvaluatedSymbol();
}
}

namespace Module
{
static const char*const SymbolsOfInterrest[] = {
	"?Initalize@@YAXXZ",
	"?Terminate@@YAXXZ",
	"?Sampler@@YAXXZ",
};

struct Info
{
	const char *name;
	const char *file;
};

template<size_t ID>
bool callFunction(const char *module)
{
	void (*init)() = reinterpret_cast<void(*)()>(JIT::getAddress(module, SymbolsOfInterrest[ID]));
	if(init)
	{
		init();
		return true;
	}

	return false;
}

bool callInit(const char *module)
{
	return callFunction<0>(module);
}

bool callShutdown(const char *module)
{
	return callFunction<1>(module);
}

bool callSampler(const char *module)
{
	return callFunction<2>(module);
}
}

void runModule(const char *name)
{
	if(!Module::callInit(name))
	{
		printf("Calling Initalize on \"%s\" failed\n", name);
	}

	if(!Module::callSampler(name))
	{
		printf("Calling Sample on \"%s\" failed\n", name);
	}

	if(!Module::callShutdown(name))
	{
		printf("Calling Terminate on \"%s\" failed\n", name);
	}
}

int main(int argc, char **argv)
{
	static const Module::Info firstFile{"Plansch",  "CM_Plansch.bc"};
	static const Module::Info secondFile{"Schwimm", "CM_Schwimm.bc"};

	if(!JIT::setup())
	{
		printf("Failed creating JIT!\n");
		return 1;
	}

	if(!JIT::addModule(secondFile.name, secondFile.file))
	{
		printf("Failed loading Module Plansch\n");
		return 2;
	}

	runModule(secondFile.name);
	return 0;
}