#!/usr/bin/env dub
/+ dub.sdl:
	name "openapigenerator"
	dependency "dyaml" version="~>0.8.0"
    dependency "handlebars" version="~>0.2.2"
    stringImportPaths "codegen"
+/

// The following copyright string also applies to this file.
string COPYRIGHT = q"EOL
/*
 * Sailfin: a Jellyfin client written using Qt
 * Copyright (C) 2021 Chris Josten and the Sailfin Contributors.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */
EOL";
import std.algorithm;
import std.array;
import std.conv;
import std.exception;
import std.file : mkdirRecurse;
import std.functional;
import std.path : buildPath, dirSeparator;
import std.parallelism : parallel;
import std.range;
import std.regex;
import std.string;
import std.stdio;
import std.uni;

import dyaml;
import handlebars.tpl;

/* 
 * Dear future (potential) employers, hereby I swear I will not brew up such unmaintainable code
 * if you would hire me and treat me well.
 */
 
/*
 * What should I use?
 * I cannot use GTAD.
 * I cannot use GTAD, because it does not support OpenAPI 3.
 *
 * What should I use?
 * I cannot use openapi-generator.
 * I cannot use openapi-generator, because it generates too much.
 *
 * …
 *
 * I was in doubt about writing my own script.
 * Since it sounds easy and fun to do.
 * I was in doubt about writing my own script.
 * Since using DLANG fills me with joy.
 *
 * |: I was even in two minds
 * But I took no risk
 * I've been thinking about writing my own script. :| [2×]
 *
 * My own script?
 * My own script?
 * My— own— script~?
 *
 * |: Is there life on Pluto?
 * Are we able to dance on the moon?
 * Is there some space between the stars I where I'm able to go to? :| [2×]
 *
 */

static this() {
	COPYRIGHT ~= q"EOS
/*
 * WARNING: THIS IS AN AUTOMATICALLY GENERATED FILE! PLEASE DO NOT EDIT THIS, AS YOUR EDITS WILL GET
 * OVERWRITTEN AT SOME POINT! 
 *
 * If there is a bug in this file, please fix the code generator used to generate this file found in
 * core/openapigenerator.d. 
 *
 * This file is generated based on Jellyfin's OpenAPI description, "openapi.json". Please update that
 * file with a newer file if needed instead of manually updating the files.
 */
EOS";
}

// CODE GENERATION SETTINGS

// File name of the CMake file this generated should generate.
string CMAKE_INCLUDE_FILE = "GeneratedSources.cmake";
string CMAKE_VAR_PREFIX = "openapi";


string INCLUDE_PREFIX = "JellyfinQt";
string SRC_PREFIX = "";

string MODEL_FOLDER = "dto";
string SUPPORT_FOLDER = "support";
string LOADER_FOLDER = "loader";
string HTTP_LOADER_FOLDER = buildPath("loader", "http");

string[string] compatAliases;
string[string] memberAliases;

static this() {
	memberAliases["id"] = "jellyfinId";
	memberAliases["static"] = "staticStreaming";
	memberAliases["auto"] = "automatic";
}

CasePolicy OPENAPI_CASING = CasePolicy.PASCAL;
static immutable string[1] CPP_NAMESPACE = ["Jellyfin"];
static immutable string[2] CPP_NAMESPACE_DTO = ["Jellyfin", "DTO"];
static immutable string[2] CPP_NAMESPACE_SUPPORT = ["Jellyfin", "Support"];
static immutable string[2] CPP_NAMESPACE_LOADER = ["Jellyfin", "Loader"];
static immutable string[3] CPP_NAMESPACE_LOADER_HTTP = ["Jellyfin", "Loader", "HTTP"];

CasePolicy CPP_FILENAME_CASING = CasePolicy.LOWER;
CasePolicy CPP_CLASS_CASING = CasePolicy.PASCAL;
CasePolicy CPP_CLASS_MEMBER_CASING = CasePolicy.CAMEL;
// Prefix for class members.
string CPP_CLASS_MEMBER_PREFIX = "m_";
CasePolicy CPP_CLASS_METHOD_CASING = CasePolicy.CAMEL;
bool GENERATE_PROPERTIES = true;

string outputDirectory = "generated";
// END CODE GENERATION SETTINGS.

static immutable MimeType MIME_APPLICATION_JSON = MimeType("application", "json");

// Implementation

enum CasePolicy {
	KEEP, // Do not modify
	PASCAL, // PascalCase
	CAMEL, // camelCase
	SNAKE, // snake_case
	SCREAMING_SNAKE, // SCREAMING_SNAKE_CASE
	LOWER, // lowercase
	UPPER // UPPERCASE
}

string USAGE = "USAGE: %s <openapi scheme>";

class CLIArgumentException : Exception {
	mixin basicExceptionCtors;
}

int main(string[] args) {
	try {
		realMain(args);
	} catch (CLIArgumentException e) {
		stderr.writeln(e.message);
		return -1;
	}
	return 0;
}

void realMain(string[] args) {
	enforce!CLIArgumentException(args.length >= 2, USAGE.format(args[0]));
	string schemeFile = args[1];
	
	if (args.length >= 3) outputDirectory = args[2];
	mkdirRecurse(buildPath(outputDirectory, "include", INCLUDE_PREFIX, MODEL_FOLDER));
	mkdirRecurse(buildPath(outputDirectory, "include", INCLUDE_PREFIX, HTTP_LOADER_FOLDER));
	mkdirRecurse(buildPath(outputDirectory, "src", SRC_PREFIX, MODEL_FOLDER));
	mkdirRecurse(buildPath(outputDirectory, "src", SRC_PREFIX, HTTP_LOADER_FOLDER));
	
	Node root = Loader.fromFile(schemeFile).load();
	Appender!(string[]) headerFiles, implementationFiles;
	
	foreach(string key, ref const Node scheme; root["components"]["schemas"]) {
		string fileBase = key.applyCasePolicy(OPENAPI_CASING, CPP_FILENAME_CASING);
		string headerFileName = buildPath(outputDirectory, "include", INCLUDE_PREFIX, MODEL_FOLDER, fileBase ~ ".h");
		string implementationFileName = buildPath(outputDirectory, "src", SRC_PREFIX, MODEL_FOLDER, fileBase ~ ".cpp");
		
		headerFiles ~= [headerFileName];
		implementationFiles ~= [implementationFileName];
		
		File headerFile = File(headerFileName, "w+");
		File implementationFile = File(implementationFileName, "w+");
		
		generateFileForSchema(key, scheme, root["components"]["schemas"], headerFile, implementationFile);
	}
	
	Appender!(Endpoint[]) endpoints;
	Node[][string] tags;
	foreach(string path, ref Node operations; root["paths"]) {
		foreach (string operation, ref Node endpoint; operations) {
			endpoint["path"] = path;
			endpoint["operation"] = operation;
			string tag;
			if ("tags" in endpoint && endpoint["tags"].length > 0) {
				tag = endpoint["tags"][0].as!string;
			} else {
				tag = "untagged";
			}
			
			if (tag in tags) {
				tags[tag] ~= endpoint;
			} else {
				tags[tag] = [endpoint];
			}
		}
	}
	
	foreach(tag, operations; tags) {
		string fileBase = tag.applyCasePolicy(OPENAPI_CASING, CPP_FILENAME_CASING);
		string headerFileName = buildPath(outputDirectory, "include", INCLUDE_PREFIX, HTTP_LOADER_FOLDER, fileBase ~ ".h");
		string implementationFileName = buildPath(outputDirectory, "src", SRC_PREFIX, HTTP_LOADER_FOLDER, fileBase ~ ".cpp");
		
		headerFiles ~= [headerFileName];
		implementationFiles ~= [implementationFileName];
		
		File headerFile = File(headerFileName, "w+");
		File implementationFile = File(implementationFileName, "w+");
		generateFileForEndpoints(operations, root["components"]["schemas"], headerFile, 
				implementationFile, endpoints, fileBase);
	}
	
	string typesHeaderPath = buildPath(outputDirectory, "include", INCLUDE_PREFIX, LOADER_FOLDER, "requesttypes.h");
	string typesImplementationPath = buildPath(outputDirectory, "src", SRC_PREFIX, LOADER_FOLDER, "requesttypes.cpp");
	File typesHeader = File(typesHeaderPath, "w+");
	File typesImplementation = File(typesImplementationPath, "w+");
	headerFiles ~= [typesHeaderPath];
	implementationFiles ~= [typesImplementationPath];
	
	writeRequestTypesFile(typesHeader, typesImplementation, endpoints[].sort!((a, b) => a.name < b.name));
	
	writeCMakeFile(headerFiles[], implementationFiles[]);
}

/**
 * Writes a CMake file that includes the specified files.
 */
void writeCMakeFile(string[] headerFiles, string[] implementationFiles) {
	File output = File(buildPath(outputDirectory, CMAKE_INCLUDE_FILE), "w+");
	output.writeln("cmake_minimum_required(VERSION 3.0)");
	// Peek laziness: wrapping a C++ comment inside a CMake block comment because I couldn't be
	// donkey'd to do otherwise.
	output.writeln("#[[");
	output.writeln(COPYRIGHT);
	output.writeln("]]");
	
	output.writef("set(%s_HEADERS", CMAKE_VAR_PREFIX);
	foreach (headerFile; headerFiles) {
		output.writeln();
		output.writef("\t%s", headerFile);
	}
	output.writeln(")");
	output.writeln();
	
	output.writef("set(%s_SOURCES", CMAKE_VAR_PREFIX);
	foreach (implementationFile; implementationFiles) {
		output.writeln();
		output.writef("\t%s", implementationFile);
	}
	output.writeln(")");
}

/**
 * Writes the file with all the types that are just used for making requests to the API endpoint
 *
 * Params:
 *     headerFile = The file to write the header to
 *     implementationFile = The file to write the implememntation to
 *     endpoints = A list of endpoints to extract request type information from.
 */
void writeRequestTypesFile(R)(File headerFile, File implementationFile, R endpoints) if(is(ElementType!R : Endpoint)) {
	
	string[] collectImports(R range, bool function(MetaTypeInfo) predicate) {
		return endpoints
				// Create a list of all parameter types used
				.map!(e => e.parameters)
				.joiner
				.map!(e => e.type)
				// Weed out container types
				.map!((MetaTypeInfo e) {
					if (e.isContainer) {
						MetaTypeInfo c = e.containerType;
						while (c.isContainer) {
							c = c.containerType;
						}
						return c;
					} else {
						return e;
					}
				})
				// Filter out the ones we need according to the predicate
				.filter!predicate
				// Map MetaTypeInfo -> typename: string
				.map!(e => e.typeName)
				// Filter out qint32 etc
				.filter!(e => !e.startsWith("qint"))
				// Sort and filter out duplicates
				.array
				.sort
				.uniq
				.array;
	}
	
	RequestParameter[] getParameters(RequestParameter[] params, bool function(RequestParameter) pred) {
		return params
				.filter!pred
				.array
				.sort!((a, b) => a.name < b.name)
				.array
				.array;
	}
	
	string[] systemImports = collectImports(endpoints, e => e.needsSystemImport) 
		~ ["QList", "QStringList", "optional"];
	string[] userImports = collectImports(endpoints, e => e.needsLocalImport)
		.map!(e => buildPath(MODEL_FOLDER, e.applyCasePolicy(CasePolicy.PASCAL, CasePolicy.LOWER) ~ ".h"))
		.array;
	
	
	
	struct EndpointController {
		string name;
		RequestParameter[] requiredPathParameters = [];
		RequestParameter[] requiredQueryParameters = [];
		RequestParameter[] optionalQueryParameters = [];
		RequestParameter[] requiredParameters = [];
		RequestParameter[] optionalParameters = [];
		RequestParameter[] parameters = [];
		RequestParameter[] bodyParameters = [];
	}
	
	struct Controller {
		EndpointController[] endpoints;
		string dtoNamespace = namespaceString!CPP_NAMESPACE_DTO;
	}
	
	Controller controller;
	
	foreach(endpoint; endpoints) {
		EndpointController endpointController;
		endpointController.name = endpoint.parameterType;
		endpointController.requiredPathParameters = 
				getParameters(endpoint.parameters, (e => e.required && e.location == ParameterLocation.PATH));
		endpointController.requiredQueryParameters =
				getParameters(endpoint.parameters, (e => e.required && e.location == ParameterLocation.QUERY));
		endpointController.optionalQueryParameters =
				getParameters(endpoint.parameters, (e => !e.required && e.location == ParameterLocation.QUERY));
		endpointController.bodyParameters = 
				getParameters(endpoint.parameters, (e => e.location == ParameterLocation.BODY));
		with (endpointController) {
			parameters = requiredPathParameters ~ requiredQueryParameters ~ optionalQueryParameters ~ bodyParameters;
			
			requiredParameters = requiredPathParameters ~ requiredQueryParameters;
			optionalParameters = optionalQueryParameters;
		}
		controller.endpoints ~= [endpointController];
	}
	headerFile.writeHeaderPreamble(CPP_NAMESPACE_LOADER, "RequestTypes", systemImports, userImports);
	headerFile.writeln(render!(import("loader_types_header.hbs"), Controller)(controller));
	headerFile.writeHeaderPostamble(CPP_NAMESPACE_LOADER, "RequestTypes");
	
	implementationFile.writeImplementationPreamble(CPP_NAMESPACE_LOADER, LOADER_FOLDER, "RequestTypes");
	implementationFile.writeln(render!(import("loader_types_implementation.hbs"), Controller)(controller));
	implementationFile.writeImplementationPostamble(CPP_NAMESPACE_LOADER, "RequestTypes");
}

/**
 * Generates files for endpoints in a category
 * Params:
 *     endpointNode = YAML node representing the endpoint.
 *     allSchemas = YAML node containing the schemas that values in the endpointNOde could reference.
 *     headerFile = File to write the header (.h) file to
 *     implementationFile = File to write the implementation (.cpp) file to.
 *     endpoints = Appender to add any requestParameters encountered to.
 *     categoryName = name of the category
 */
void generateFileForEndpoints(ref const Node[] endpointNodes, 
		ref const Node allSchemas, ref scope File headerFile, ref scope File implementationFile, 
		ref scope Appender!(Endpoint[]) endpoints, ref const string categoryName) {
	
	class EndpointController {
		string className;
		//MetaTypeInfo[] properties;
		string responseType = "void";
		string parameterType = "void";
		Endpoint endpoint;
		string operation;
		
		string pathStringInterpolation() {
			string result = "QStringLiteral(\"" ~ endpoint.path ~ "\")";
			foreach(p; endpoint.parameters.filter!(p => p.location == ParameterLocation.PATH)) {
				result = result.replace("{" ~ p.name ~ "}", "\") + Support::toString< " 
					~ p.type.typeNameWithQualifiers ~">(params." ~ p.type.name 
					~ "()) + QStringLiteral(\"");
			}
			result = result.replace(`+ QStringLiteral("")`, "");
			return result;
		}
	}
	Appender!(EndpointController[]) endpointControllers = appender!(EndpointController[]);
	endpointControllers.reserve(endpointNodes.length);
	
	string[] systemImports = ["optional"];
	string[] userImports = [
			buildPath(SUPPORT_FOLDER, "jsonconv.h"),
			buildPath(SUPPORT_FOLDER, "loader.h"), 
			buildPath(LOADER_FOLDER, "requesttypes.h")
	];
	
	foreach(endpointNode; endpointNodes) {
		string name = endpointNode["operationId"].as!string;
		
		Endpoint endpoint = new Endpoint();
		endpoint.name = name;
		endpoint.parameterType = name ~ "Params";
		endpoint.description = endpointNode.getOr!string("summary", "");
		endpoint.path = endpointNode["path"].as!string;
		endpoint.operation = endpointNode["operation"].as!string.toLower();
		
		// Find the most likely result response.
		foreach(string code, const Node response; endpointNode["responses"]) {
			int codeNo = to!int(code);
			if ([200, 201].canFind(codeNo)) {
				foreach(string contentType, const Node content; response["content"]) {
					if (contentType == "application/json") {
						endpoint.hasSuccessResponse = true;
						if ("$ref" in content["schema"]) {
							string reference = content["schema"]["$ref"].as!string.chompPrefix("#/components/schemas/");
							endpoint.resultIsReference = true;
							endpoint.resultType = reference;
							userImports ~= [buildPath(MODEL_FOLDER, reference.applyCasePolicy(CasePolicy.PASCAL, CasePolicy.LOWER) ~ ".h")];
						} else if ("schema" in content){
							endpoint.resultIsReference = false;
							string typeName = endpoint.name ~ "Response";
							MetaTypeInfo responseType = getType(typeName, content["schema"], allSchemas);
							endpoint.resultType = responseType.typeName;
							if (responseType.needsLocalImport && !responseType.isContainer) {
								userImports ~= [buildPath(MODEL_FOLDER, endpoint.resultType)];
							}
							
							MetaTypeInfo t = responseType;
							while(t.isContainer) {
								t = t.containerType;
								if (t.needsLocalImport) {
									userImports ~= [buildPath(MODEL_FOLDER, t.fileName)];
								} else if (t.needsSystemImport && !t.isContainer){
									systemImports ~= [t.typeName];
								}
							} 
						}
					}
				}
			}
			if (codeNo == 204 /* No content */) {
				endpoint.resultType = "void";
				endpoint.hasSuccessResponse = true;
			}
		}
		
		if ("requestBody" in endpointNode) {
			string description = "";
			if ("description" in endpointNode["requestBody"]) {
				description = endpointNode["requestBody"]["description"].as!string;
			}
			
			MimeType[] similarMimeTypes = [];
			
			foreach(string contentType, const Node content; endpointNode["requestBody"]["content"]) {
				MimeType mimeType = MimeType.parse(contentType);
				// Skip if we already had a similar mime type before.
				if (similarMimeTypes.any!((t) => t.compatible(mimeType))) continue;
				similarMimeTypes ~= [mimeType];
			
				RequestParameter param = new RequestParameter();
				param.location = ParameterLocation.BODY;
				param.description = description;
				param.required = true;
				
				// Hardcode this because the openapi description of Jellyfin seems to contain both
				if (mimeType.type == "text" && mimeType.subtype == "json") continue;
				
				if (mimeType.compatible(MIME_APPLICATION_JSON)) {
					string name = "body";
					param.type = getType(name, content["schema"], allSchemas);
				} else {
					MetaTypeInfo info = new MetaTypeInfo();
					info.name = "body";
					info.originalName = "body";
					info.typeName = "QByteArray";
					info.isTypeNullable = true;
					info.needsSystemImport = true;
					info.typeNullableCheck = ".isEmpty()";
					info.typeNullableSetter = ".clear()";
					param.type = info;
				}
				endpoint.bodyParameters ~= [param];
				endpoint.parameters ~= [param];
			}
		}
		
		// Build the parameter structure.
		if ("parameters" in endpointNode && endpointNode["parameters"].length > 0) {
			foreach (ref const Node yamlParameter; endpointNode["parameters"]) {
				RequestParameter param = new RequestParameter();
				param.name = yamlParameter["name"].as!string;
				param.required = yamlParameter.getOr!bool("required", false);
				param.description = yamlParameter.getOr!string("description", "");
				
				param.type = getType(param.name, yamlParameter["schema"], allSchemas);
				if (!param.type.isNullable && !param.required && !param.type.hasDefaultValue) {
					param.type.isNullable = true;
				}
				switch(yamlParameter["in"].as!string.toLower) {
				case "path":
					param.location = ParameterLocation.PATH;
					endpoint.requiredPathParameters ~= [param];
					break;
				case "query":
					param.location = ParameterLocation.QUERY;
					if (param.required) {
						endpoint.requiredQueryParameters ~= [param];
					} else {
						endpoint.optionalQueryParameters ~= [param];
					}
					break;
				default:
					assert(false);
				}
				endpoint.parameters ~= [param];
			}
		}
		
		endpoints ~= [endpoint];
		
		EndpointController endpointController = new EndpointController();
		endpointController.className = name.applyCasePolicy(OPENAPI_CASING, CPP_CLASS_CASING);
		endpointController.endpoint = endpoint;
		endpointController.operation = endpoint.operation.applyCasePolicy(CasePolicy.CAMEL, CasePolicy.PASCAL);
		endpointControllers ~= [endpointController];
	}
	
	
	// Render templates
	struct Controller {
		EndpointController[] endpoints;
		string supportNamespace = namespaceString!CPP_NAMESPACE_SUPPORT;
		string dtoNamespace = namespaceString!CPP_NAMESPACE_DTO;
	}
	
	Controller controller = Controller();
	controller.endpoints = endpointControllers[];
	
	writeHeaderPreamble(headerFile, CPP_NAMESPACE_LOADER_HTTP, categoryName, systemImports, userImports);
	headerFile.writeln(render!(import("loader_header.hbs"), Controller)(controller));
	writeHeaderPostamble(headerFile, CPP_NAMESPACE_LOADER_HTTP, categoryName);
	
	writeImplementationPreamble(implementationFile, CPP_NAMESPACE_LOADER_HTTP, HTTP_LOADER_FOLDER, categoryName);
	implementationFile.writeln(render!(import("loader_implementation.hbs"), Controller)(controller));
	writeImplementationPostamble(implementationFile, CPP_NAMESPACE_LOADER_HTTP, categoryName);
}

/**
 * Generates a file containing a class generated based on the given JSON Schema.
 */
void generateFileForSchema(ref const string name, ref const Node scheme, Node allSchemas, 
		ref scope File headerFile, ref scope File implementationFile) {
	
	// Check if this JSON "thing" is an enum
	if ("enum" in scheme) {
		string[3] imports = ["QJsonValue", "QObject", "QString"];
		string[1] userImports = [buildPath(SUPPORT_FOLDER, "jsonconv.h")];
		
		Appender!(EnumEntry[]) entries;
		foreach (string value; scheme["enum"]) {
			EnumEntry entry = { value, value };
			if (string* correctedName = value in memberAliases) {
				entry.name = *correctedName;
			}
			if (entry.name[0].isLower()) {
				// QML does not like enumeration starting with a lower case letter
				entry.name = ([entry.name[0].toUpper()] ~ entry.name[1..$].to!(dchar[])).to!string;
			}
			entries ~= entry;
		}
		
		writeHeaderPreamble(headerFile, CPP_NAMESPACE_DTO, name, imports, userImports);
		writeEnumHeader(headerFile, name, entries[]);
		writeHeaderPostamble(headerFile, CPP_NAMESPACE_DTO, name);
		
		writeImplementationPreamble(implementationFile, CPP_NAMESPACE_DTO, MODEL_FOLDER, name);
		writeEnumImplementation(implementationFile, name, entries[]);
		writeImplementationPostamble(implementationFile, CPP_NAMESPACE_DTO, name);
	}
	
	// Check if this is an object
	if (scheme["type"].as!string == "object" && "properties" in scheme) {
		// Determine all imports
		Appender!(string[]) systemImports, userImports;
		Appender!(string[]) forwardDeclarations;
		systemImports ~= ["optional", "QJsonObject", "QJsonValue"];
		userImports ~= [buildPath(SUPPORT_FOLDER, "jsonconv.h")];
		
		MetaTypeInfo[] usedTypes = collectTypeInfo(scheme["properties"], allSchemas);
		usedTypes[$-1].isLast = true;
		bool importedContainers = false;
		void collectImports(MetaTypeInfo type) {
			if (type.needsPointer && !systemImports[].canFind("QSharedPointer")) {
				systemImports ~= ["QSharedPointer"];
			}
			
			if (type.needsSystemImport && !systemImports[].canFind(type.typeName)) {
				if (type.isContainer) {
					if (!importedContainers) {
						systemImports ~= ["QList", "QStringList"];
						importedContainers = true;
					}
					if (type.containerType !is null) {
						collectImports(type.containerType);
					}
				} else {
					systemImports ~= [type.typeName];
				}
			} else {
				string userImport = buildPath(MODEL_FOLDER, type.typeName.applyCasePolicy(OPENAPI_CASING, CasePolicy.LOWER) ~ ".h");
				if (type.needsLocalImport && !userImports[].canFind(userImport) && type.typeName != name) {
					userImports ~= userImport;
				}
			}
		}
		foreach (type; usedTypes) {
			collectImports(type);
		}

		foreach (ref type; usedTypes.retro) {
			if (type.isNotNullable) {
				type.isLastNonNullable = true;
				break;
			}
		}
		
		// Sort them for nicer reading
		string[] sortedSystemImports = sort(systemImports[]).array;
		string[] sortedUserImports = sort(userImports[]).array;
		string[] sortedForwardDeclarations = sort(forwardDeclarations[]).array;
		
		// Write implementation files
		writeHeaderPreamble(headerFile, CPP_NAMESPACE_DTO, name, sortedSystemImports, sortedUserImports);
		writeObjectHeader(headerFile, name, usedTypes, sortedForwardDeclarations);
		writeHeaderPostamble(headerFile, CPP_NAMESPACE_DTO, name);
		
		writeImplementationPreamble(implementationFile, CPP_NAMESPACE_DTO, MODEL_FOLDER, name);
		writeObjectImplementation(implementationFile, name, usedTypes);
		writeImplementationPostamble(implementationFile, CPP_NAMESPACE_DTO, name);
	} else if (scheme["type"] == "object") {
		// Write implementation files
		writeHeaderPreamble(headerFile, CPP_NAMESPACE_DTO, name, ["QJsonObject"]);
		headerFile.writefln("using %s = QJsonObject;", name);
		writeHeaderPostamble(headerFile, CPP_NAMESPACE_DTO, name);
		
		writeImplementationPreamble(implementationFile, CPP_NAMESPACE_DTO, MODEL_FOLDER, name);
		headerFile.writeln("// No implementation needed");
		writeImplementationPostamble(implementationFile, CPP_NAMESPACE_DTO, name);
	}
}

// Object

// We need to recurse (sometimes)
/**
 * Create a MetaTypeInfo object based on a JSON schema
 *
 * In the future, this implementation should use some form of configuration file
 * which contains data about the built-in types, since hard-coding doesn't seem like a
 * good idea.
 *
 * Params:
 *     name = The name of this object
 *     node = The node containing the JSON Schema of this object
 *     allSchemas = The node containing the a map of names to JSON Schemas, which the node
 *                  parameter could refrence.
 */
MetaTypeInfo getType(ref const string name, const ref Node node, const ref Node allSchemas) {
	MetaTypeInfo[] allOf = [];
	
	MetaTypeInfo info = new MetaTypeInfo();
	info.originalName = name;
	info.name = name.applyCasePolicy(OPENAPI_CASING, CPP_CLASS_MEMBER_CASING);
	
	if (const Node* allOfNodes = "allOf" in node) {
		writeln(name, " contains allOf:");
		allOf.reserve(node.length);
		foreach (const ref Node anotherType; *allOfNodes) {
			allOf ~= getType(name, anotherType, allSchemas);
			writeln(" - ", allOf[$ - 1].name);
		}
	}
	
	if (allOf.length == 1 && allOf[0].name == info.name) {
		// If it contains a single allOf value, just replace our current type with it.
		return allOf[0];
	}
	
	info.defaultValue = node.getOr!string("default", "");
	
	if ("description" in node) {
		info.description = node["description"].as!string;
	}
	
	// Special case for QML
	info.name = memberAliases.get(info.name.toLower(), info.name);
	
	// Check if this schema is a reference to another schema
	if ("$ref" in node) {
		string type = node["$ref"].as!string()["#/components/schemas/".length..$];
		info.needsLocalImport = true;
		info.typeName = type;
		if (type in allSchemas) {
			if ("enum" in allSchemas[type]) {
				info.isTypeNullable = true;
				info.typeNullableCheck = "== " ~ info.typeName ~ "::EnumNotSet";
				info.typeNullableSetter = "= " ~ info.typeName ~ "::EnumNotSet";
			} else if ("type" in allSchemas[type] 
					&& allSchemas[type]["type"].as!string == "object") {
				info.needsPointer = true;
				info.isTypeNullable = true;
				info.typeNullableCheck = ".isNull()";
				info.typeNullableSetter = ".clear()";
			}
		}
		return info;
	}
	
	// Type is an enumeration
	if ("enum" in node && !("type" in node)) {
		info.isTypeNullable = true;
		info.typeNullableCheck = "== " ~ info.typeName ~ "::EnumNotSet";
		info.typeNullableSetter = "= " ~ info.typeName ~ "::EnumNotSet";
		return info;
	}
	
	// No type information specified. As a fallback, use a QVariant.
	if (!("type" in node)) {
		info.typeName = "QVariant";
		info.isTypeNullable = true;
		info.needsSystemImport = true;
		info.typeNullableCheck = ".isNull()";
		info.typeNullableSetter = ".clear()";
		return info;
	}
	
	info.isNullable = node.getOr!bool("nullable", false);
	switch(node["type"].as!string) {
	case "boolean":
		info.typeName = "bool";
		return info;
	case "string":
		if ("format" in node) {
			switch(node["format"].as!string) {
			case "date-time":
				info.typeName= "QDateTime";
				info.needsSystemImport = true;
				info.isTypeNullable = true;
				info.typeNullableCheck = ".isNull()";
				info.typeNullableSetter = "= QDateTime()";
				return info;
			/+case "uuid":
				info.typeName = "QUuid";
				info.needsSystemImport = true;
				info.isTypeNullable = true;
				info.typeNullableCheck = ".isNull()";
				info.typeNullableSetter = "= QGuid()";
				return info;+/
			default:
				break;
			}
		}
		info.isTypeNullable = true;
		info.typeName = "QString";
		info.needsSystemImport = true;
		info.typeNullableCheck = ".isNull()";
		info.typeNullableSetter = ".clear()";
		return info;
	case "integer":
		if ("format" in node) {
			info.typeName= "q" ~ node["format"].as!string;
			return info;
		}
		goto default;
	case "number":
		if ("format" in node) {
			switch(node["format"].as!string) {
			case "float":
			case "double":
				info.typeName = node["format"].as!string;
				return info;
			default:
				break;
			}
		}
		goto default;
	case "object":
		info.typeName = "QJsonObject"; // This'll do for now
		info.isTypeNullable = true;
		info.typeNullableCheck = ".isEmpty()";
		info.typeNullableSetter = "= QJsonObject()";
		return info;
	case "array":
		string containedTypeName = "arrayItem";
		MetaTypeInfo containedType = getType(containedTypeName, node["items"], allSchemas);
		containedType.needsPointer = false;
		info.needsLocalImport = containedType.needsLocalImport;
		info.needsSystemImport = true;
		info.isContainer = true;
		info.containerType = containedType;
		info.isTypeNullable = true;
		info.typeNullableCheck = ".size() == 0";
		info.typeNullableSetter = ".clear()";
		if (containedType.typeName == "QString") {
			info.typeName = "QStringList";
		} else {
			info.typeName = "QList<" ~ containedType.typeNameWithQualifiers ~ ">";
		}
		return info;
	default:
		info.typeName = "UNIMPLEMENTED";
		return info;
	}
}

/**
 * Given a list of JSON schemes, this will generate a list of MetaTypeInfo[]
 */
MetaTypeInfo[] collectTypeInfo(const ref Node properties, const ref Node allSchemas) {
	Appender!(MetaTypeInfo[]) result;
	
	foreach(ref string name, const ref Node node; properties) {
		result ~= getType(name, node, allSchemas);
	}
	return result[];
}

void writeObjectHeader(File output, string name, MetaTypeInfo[] properties, string[] userImports) {
	class Controller {
		string className;
		MetaTypeInfo[] properties;
		string[] userImports;
		string supportNamespace = namespaceString!CPP_NAMESPACE_SUPPORT;
		bool hasRequiredProperties;
	}
	Controller controller = new Controller();
	controller.className = name.applyCasePolicy(OPENAPI_CASING, CPP_CLASS_CASING);
	controller.properties = properties;
	controller.userImports = userImports;
	controller.hasRequiredProperties = properties.canFind!((x) => !x.isNullable);
	
	output.writeln(render!(import("object_header.hbs"), Controller)(controller));
	
	
}

void writeObjectImplementation(File output, string name, MetaTypeInfo[] properties) {
	class Controller {
		string className;
		MetaTypeInfo[] properties;
		string supportNamespace = namespaceString!CPP_NAMESPACE_SUPPORT;
		bool hasRequiredProperties;
	}
	Controller controller = new Controller();
	controller.className = name.applyCasePolicy(OPENAPI_CASING, CPP_CLASS_CASING);
	controller.properties = properties;
	controller.hasRequiredProperties = properties.canFind!((x) => !x.isNullable);

	output.writeln(render!(import("object_implementation.hbs"), Controller)(controller));
}

// Enum
void writeEnumHeader(File output, string name, EnumEntry[] entries, string doc = "") {
	class Controller {
		string className;
		EnumEntry[] entries;
		string supportNamespace = namespaceString!CPP_NAMESPACE_SUPPORT;
	}
	Controller controller = new Controller();
	controller.className = name.applyCasePolicy(OPENAPI_CASING, CPP_CLASS_CASING);
	controller.entries = entries;
	output.writeln(render!(import("enum_header.hbs"), Controller)(controller));
}

void writeEnumImplementation(File output, string name, EnumEntry[] entries) {
	class Controller {
		string className;
		EnumEntry[] entries;
		string supportNamespace = namespaceString!CPP_NAMESPACE_SUPPORT;
	}
	Controller controller = new Controller();
	controller.className = name.applyCasePolicy(OPENAPI_CASING, CPP_CLASS_CASING);
	controller.entries = entries;
	output.writeln(render!(import("enum_implementation.hbs"), Controller)(controller));
}

// Common
void writeHeaderPreamble(File output, immutable string[] fileNamespace, string className, string[] imports = [], string[] userImports = []) {
	output.writeln(COPYRIGHT);
	string guard = guardName(fileNamespace, className);
	output.writefln("#ifndef %s", guard);
	output.writefln("#define %s", guard);
	output.writeln();
	
	foreach(file; imports) {
		output.writefln("#include <%s>", file);
	}
	
	if (imports.length > 0) output.writeln();
	
	foreach(file; userImports) {
		output.writefln("#include \"%s\"", buildPath(INCLUDE_PREFIX, file));
	}
	if (userImports.length > 0) output.writeln();
	
	// FIXME: Should be configurable
	output.writefln("namespace Jellyfin {");
	output.writefln("// Forward declaration");
	output.writefln("class ApiClient;");
	output.writefln("}");
	
	foreach (namespace; fileNamespace) {
		output.writefln("namespace %s {", namespace);
	}
	output.writeln();
}

void writeHeaderPostamble(File output, immutable string[] fileNamespace, string className) {
	output.writeln();
	foreach_reverse(namespace; fileNamespace) {
		output.writefln("} // NS %s", namespace);
	}
	output.writeln();
	output.writefln("#endif // %s", guardName(fileNamespace, className));
}

void writeImplementationPreamble(File output, immutable string[] fileNamespace, string folder, string className, string[] imports = []) {
	output.writeln(COPYRIGHT);
	output.writefln("#include <%s>", buildPath(INCLUDE_PREFIX, folder, className.applyCasePolicy(OPENAPI_CASING, CasePolicy.LOWER) ~ ".h"));
	output.writeln();
	
	foreach(file; imports) {
		output.writefln("#include <%s>", buildPath(INCLUDE_PREFIX, file));
	}
	if (imports.length > 0) output.writeln();
	
	foreach (namespace; fileNamespace) {
		output.writefln("namespace %s {", namespace);
	}
	output.writeln();
}

void writeImplementationPostamble(File output, immutable string[] fileNamespace, string className) {
	output.writeln();
	foreach_reverse(namespace; fileNamespace) {
		output.writefln("} // NS %s", namespace);
	}
}


// Helper functions

/**
 * Transforsm the given string from the input casing system to the ouptut casing system.
 *
 * Params:
 *     source = The string to transform
 *     
 */
string applyCasePolicy(string source, CasePolicy input, CasePolicy output) {
	if (input == output)  return source;
	switch(output) {
	case CasePolicy.KEEP:
		return source;
	case CasePolicy.PASCAL:
		if (input == CasePolicy.CAMEL) {
			char[] result = source.dup;
			result[0] = cast(char) toUpper(result[0]);
			return cast(string) result;
		} else {
			throw new Exception("Not implemented");
		}
	case CasePolicy.CAMEL:
		if (input == CasePolicy.PASCAL) {
			char[] result = source.dup;
			result[0] = cast(char) toLower(result[0]);
			return cast(string) result;
		} else {
			throw new Exception("Not implemented");
		}
	case CasePolicy.LOWER:
		if (input == CasePolicy.CAMEL || input == CasePolicy.PASCAL) {
			return source.toLower();
		}
		throw new Exception("Not implemented");
	case CasePolicy.UPPER:
		if (input == CasePolicy.CAMEL || input == CasePolicy.PASCAL) {
			return source.toUpper();
		}
		throw new Exception("Not implemented");
	case CasePolicy.SCREAMING_SNAKE:
		if (input == CasePolicy.CAMEL || input == CasePolicy.PASCAL) {
			Appender!(char[]) result;
			foreach(window; source.slide!(Yes.withPartial)(2)) {
				dchar c = window.front;
				window.popFront();
				dchar n = window.front;
				if (isLower(c) && !isLower(n)) {
					result ~= toUpper(c);
					result ~= '_';
				} else {
					result ~= toUpper(c);
				} 
			}
			result ~= toUpper(source[$ - 1]);
			return cast(string) result[];
		}
		throw new Exception("Not implemented");
	default:
		throw new Exception("Not implemented");
	}
}

unittest {
	assert("fooBar".applyCasePolicy(CasePolicy.CAMEL, CasePolicy.SNAKE) == "foo_bar");
}

class MetaTypeInfo {
public:
	string originalName = "";
	string name = "";
	string typeName = "";
	/// Description of this property.
	string description = "";
	/// If this property is nullable according to the OpenAPI spec.
	bool isNullable = false;
	bool needsPointer = false;
	/// If the type needs a system import (Such as Qt types)
	bool needsSystemImport = false;
	/// If the type needs a local import (such as types elsewhere in this project).
	bool needsLocalImport = false;
	/// If the type is a container type
	bool isContainer = false;
	/// If this type has a non-ambigious null state.
	bool isTypeNullable = false;
	/// If `isContainer` is true, the type of the container.
	MetaTypeInfo containerType = null;
	
	/// For use in templating
	bool isLast = false;
	bool isLastNonNullable = false;
	string defaultValue = "";
	
	bool hasDefaultValue() const {
		return defaultValue.length > 0;
	}
	
	string writeName() const {
		return name.applyCasePolicy(CPP_CLASS_MEMBER_CASING, CasePolicy.PASCAL);
	}
	
	string memberName() const {
		return CPP_CLASS_MEMBER_PREFIX ~ name;
	}
	
	string typeNameWithQualifiers() const {
		if (needsPointer) {
			return "QSharedPointer<" ~ typeName ~ ">";
		}
		if (needsOptional) {
			return "std::optional<" ~ typeName ~ ">";
		} else {
			return typeName;
		}
	}
	
	bool needsOptional() const {
		return (isNullable || hasDefaultValue) && !isTypeNullable;
	}
	
	string typeNullableCheck;
	string nullableCheck() const {
		if (needsOptional) {
			return "!" ~ memberName ~ ".has_value()";
		} else if (typeNullableCheck.length > 0) {
			return memberName  ~ typeNullableCheck;
		}
		
		return "Q_ASSERT(false)";
	}
	
	string typeNullableSetter = "";
	string nullableSetter() const {
		if (needsOptional) {
			return " = std::nullopt";
		}
		return typeNullableSetter;
	}
	
	string defaultInitializer() const {
		if (needsPointer) return "QSharedPointer<" ~ typeName ~ ">()";
		if (needsOptional) return "std::nullopt";
		return "";
	}

	bool isNotNullable() const { return !isNullable; }
	
	string fileName (){
		return typeName.applyCasePolicy(CasePolicy.PASCAL, CasePolicy.LOWER) ~ ".h";
	}
}

/**
 * Represents an API endpoint.
 */
class Endpoint {
	bool resultIsReference = false;
	bool hasSuccessResponse = false;
	string name;
	
	/// The type of the 
	string resultType;
	
	/// The name of the structure containing the parameters for this endpoint.
	string parameterType = "void";
	
	/// HTTP path for this endpoint.
	string path;
	
	/// Description/documentation for this endpoint
	string description;
	
	/// HTTP method for this endpoint
	string operation;
	
	/// List of all parameters for this request
	RequestParameter[] parameters = [];
	
	RequestParameter[] requiredPathParameters;
	RequestParameter[] requiredQueryParameters;
	RequestParameter[] optionalQueryParameters;
	RequestParameter[] bodyParameters;
}

enum ParameterLocation {
	PATH,
	QUERY,
	COOKIE,
	HEADER,
	BODY
}

class RequestParameter {
	string name;
	ParameterLocation location;
	bool required;
	string description;
	MetaTypeInfo type;
	// Only for body parameters.
	string mimeType;
}

struct EnumEntry {
	string name;
	string value;
}

/**
 * Generates a guard name based on a namespace and class name.
 *
 * Params:
 *     namespace =  Array of namespaces this class is in.
 *     className =  The name of this class (or enum or whatever).
 */
string guardName(immutable string[] namespace, string className) {
	return namespace.map!toUpper().join("_") ~ "_"
		~ className.applyCasePolicy(OPENAPI_CASING, CasePolicy.UPPER)
		~ "_H";
}

unittest {
	assert(guardName(["foo", "bar", "Baz"] == "FOO_BAR_BAZ_H")
}

/**
 * Converts a string array of namespaces into a C++ namespace string.
 */
string namespaceString(string[] name)() {
	string result;
	static foreach(idx, part; name) {
		static if (idx != 0) {
			result ~= "::";
		}
		result ~= part;
	}
	return result;
}

/// Ditto
string namespaceString(immutable string[] name) {
	string result;
	foreach(idx, part; name) {
		if (idx != 0) {
			result ~= "::";
		}
		result ~= part;
	}
	return result;
}

unittest {
	assert(namespaceString!["foo", "bar"] == "foo::bar");
}


bool areMimesCompatible(string expectedMime, string mime) @safe nothrow {
	try {
		MimeType type1 = MimeType.parse(expectedMime);
		MimeType type2 = MimeType.parse(mime);
		return type1.compatible(type2);
	} catch(Exception e) {
		return false;
	}
}

class MimeParseException : Exception {
	mixin basicExceptionCtors;
}

/**
 * Data structure representing a mime type.
 */
struct MimeType {
public:
	
	this(string type, string subtype, string suffix = null, string parameter = null) @safe {
		this.type = type;
		this.subtype = subtype;
		this.suffix = suffix;
		this.parameter = parameter;
	}
	
	string toString() const {
		Appender!string result = appender!string;
		result.reserve(type.length + subtype.length + (suffix ? suffix.length + 1 : 0) + (parameter ? parameter.length + 2 : 0));
		result ~= type;
		result ~= "/";
		result ~= subtype;
		if (suffix) {
			result ~= "+";
			result ~= suffix;
		}
		if (parameter) {
			result ~= "; ";
			result ~= parameter;
		}
		return result[];
	}
	
	static MimeType parse(string mime) @safe {
		auto parts = mime.matchFirst(mimeRegex);
		enforce(parts, mime ~ " is not a valid mimetype");
		return MimeType(parts["type"], parts["subtype"], parts["suffix"], parts["parameter"]);
	}
	
	static auto mimeRegex = regex(`(?P<type>\w*)\/(?P<subtype>(([\w\-]+\.)+)?[\w\-\*]+)(\+(?P<suffix>[\w\-\.]+))?(; (?P<parameter>[\w+-\.=]+))?`);

	string type = null;
	string subtype = null;;
	string suffix = null;
	string parameter = null;
	
	/**
	* Checks if two mime types are compatible. 
	*
	* Two mime types are considered compatible if either one of the following 
	* conditions is true:
	* 1. The mime types are exactly the same
	* 2. The expected mime type is in the form "x/*" and the mime type is in the form "x/y"
	* 3. The expected mime type is in the form "x/z+y" and the mime type is in the form "x/y"
	* 4. The expected mime type is in the form "x/y" and the mime type is in the form "x/z+y"
	*/
	bool compatible(const ref MimeType other) const @safe {
		if (type != other.type) return false;
		if (subtype == other.subtype || subtype == "*" || other.subtype == "*") return true;
		if (subtype == other.suffix || suffix == other.subtype) return true;
		return false;
	}
}

/**
 * Retrieves the given key from the node, if it does not exists, returns the or parameter.
 */
T getOr(T)(const ref Node node, string key, T or) {
	if (key in node) {
		try {
			return node[key].get!T;
		} catch (Exception e) {
			return or;
		}
	} else {
		//stdout.writefln("Could not find %s", key); 
		return or;
	}
}
