import EleventyBaseError from "./Errors/EleventyBaseError.js";
import EleventyExtensionMap from "./EleventyExtensionMap.js";
import TemplateEngineManager from "./Engines/TemplateEngineManager.js";
import CustomEngine from "./Engines/Custom.js";

// import debugUtil from "debug";
// const debug = debugUtil("Eleventy:TemplateRender");

class TemplateRenderConfigError extends EleventyBaseError {}
class TemplateRenderUnknownEngineError extends EleventyBaseError {}

// works with full path names or short engine name
class TemplateRender {
	constructor(tmplPath, config) {
		if (!tmplPath) {
			throw new Error(`TemplateRender requires a tmplPath argument, instead of ${tmplPath}`);
		}
		if (!config) {
			throw new TemplateRenderConfigError("Missing `config` argument.");
		}
		if (config.constructor.name === "TemplateConfig") {
			this.eleventyConfig = config;
			this.config = config.getConfig();
		} else {
			throw new Error("Third argument to TemplateRender must be a TemplateConfig instance.");
		}

		this.engineNameOrPath = tmplPath;

		this.parseMarkdownWith = this.config.markdownTemplateEngine;
		this.parseHtmlWith = this.config.htmlTemplateEngine;
	}

	get dirs() {
		return this.eleventyConfig.directories;
	}

	get inputDir() {
		return this.dirs.input;
	}

	get includesDir() {
		return this.dirs.includes;
	}

	/* Backwards compat */
	getIncludesDir() {
		return this.includesDir;
	}

	get config() {
		return this._config;
	}

	set config(config) {
		this._config = config;
	}

	set extensionMap(extensionMap) {
		this._extensionMap = extensionMap;
	}

	get extensionMap() {
		if (!this._extensionMap) {
			this._extensionMap = new EleventyExtensionMap(this.eleventyConfig);
			this._extensionMap.setFormats([]);
		}
		return this._extensionMap;
	}

	async getEngineByName(name) {
		let engine = await this.extensionMap.engineManager.getEngine(name, this.extensionMap);
		engine.eleventyConfig = this.eleventyConfig;

		return engine;
	}

	// Runs once per template
	async init(engineNameOrPath) {
		let name = engineNameOrPath || this.engineNameOrPath;
		this.extensionMap.config = this.eleventyConfig;

		let extensionEntry = this.extensionMap.getExtensionEntry(name);
		let engineName = extensionEntry?.aliasKey || extensionEntry?.key;
		if (TemplateEngineManager.isSimpleAlias(extensionEntry)) {
			engineName = extensionEntry?.key;
		}
		this._engineName = engineName;

		if (!extensionEntry || !this._engineName) {
			throw new TemplateRenderUnknownEngineError(
				`Unknown engine for ${name} (supported extensions: ${this.extensionMap.getReadableFileExtensions()})`,
			);
		}

		this._engine = await this.getEngineByName(this._engineName);

		if (this.useMarkdown === undefined) {
			this.setUseMarkdown(this._engineName === "md");
		}
	}

	get engineName() {
		if (!this._engineName) {
			throw new Error("TemplateRender needs a call to the init() method.");
		}
		return this._engineName;
	}

	get engine() {
		if (!this._engine) {
			throw new Error("TemplateRender needs a call to the init() method.");
		}
		return this._engine;
	}

	static parseEngineOverrides(engineName) {
		if (typeof (engineName || "") !== "string") {
			throw new Error("Expected String passed to parseEngineOverrides. Received: " + engineName);
		}

		let overlappingEngineWarningCount = 0;
		let engines = [];
		let uniqueLookup = {};
		let usingMarkdown = false;
		(engineName || "")
			.split(",")
			.map((name) => {
				return name.toLowerCase().trim();
			})
			.forEach((name) => {
				// html is assumed (treated as plaintext by the system)
				if (!name || name === "html") {
					return;
				}

				if (name === "md") {
					usingMarkdown = true;
					return;
				}

				if (!uniqueLookup[name]) {
					engines.push(name);
					uniqueLookup[name] = true;

					// we already short circuit md and html types above
					overlappingEngineWarningCount++;
				}
			});

		if (overlappingEngineWarningCount > 1) {
			throw new Error(
				`Don’t mix multiple templating engines in your front matter overrides (exceptions for HTML and Markdown). You used: ${engineName}`,
			);
		}

		// markdown should always be first
		if (usingMarkdown) {
			engines.unshift("md");
		}

		return engines;
	}

	// used for error logging and console output.
	getReadableEnginesList() {
		return this.getReadableEnginesListDifferingFromFileExtension() || this.engineName;
	}

	getReadableEnginesListDifferingFromFileExtension() {
		let keyFromFilename = this.extensionMap.getKey(this.engineNameOrPath);
		if (this.engine instanceof CustomEngine) {
			if (
				this.engine.entry &&
				this.engine.entry.name &&
				keyFromFilename !== this.engine.entry.name
			) {
				return this.engine.entry.name;
			} else {
				// We don’t have a name for it so we return nothing so we don’t misreport (per #2386)
				return;
			}
		}

		if (this.engineName === "md" && this.useMarkdown && this.parseMarkdownWith) {
			return this.parseMarkdownWith;
		}
		if (this.engineName === "html" && this.parseHtmlWith) {
			return this.parseHtmlWith;
		}

		// templateEngineOverride in play and template language differs from file extension
		if (keyFromFilename !== this.engineName) {
			return this.engineName;
		}
	}

	// TODO templateEngineOverride
	getPreprocessorEngine() {
		if (this.engineName === "md" && this.parseMarkdownWith) {
			return this.parseMarkdownWith;
		}
		if (this.engineName === "html" && this.parseHtmlWith) {
			return this.parseHtmlWith;
		}
		return this.extensionMap.getKey(this.engineNameOrPath);
	}

	// We pass in templateEngineOverride here because it isn’t yet applied to templateRender
	getEnginesList(engineOverride) {
		if (engineOverride) {
			let engines = TemplateRender.parseEngineOverrides(engineOverride).reverse();
			return engines.join(",");
		}

		if (this.engineName === "md" && this.useMarkdown && this.parseMarkdownWith) {
			return `${this.parseMarkdownWith},md`;
		}
		if (this.engineName === "html" && this.parseHtmlWith) {
			return this.parseHtmlWith;
		}

		// templateEngineOverride in play
		return this.extensionMap.getKey(this.engineNameOrPath);
	}

	async setEngineOverride(engineName, bypassMarkdown) {
		let engines = TemplateRender.parseEngineOverrides(engineName);

		// when overriding, Template Engines with HTML will instead use the Template Engine as primary and output HTML
		// So any HTML engine usage here will never use a preprocessor templating engine.
		this.setHtmlEngine(false);

		if (!engines.length) {
			await this.init("html");
			return;
		}

		await this.init(engines[0]);

		let usingMarkdown = engines[0] === "md" && !bypassMarkdown;

		this.setUseMarkdown(usingMarkdown);

		if (usingMarkdown) {
			// false means only parse markdown and not with a preprocessor template engine
			this.setMarkdownEngine(engines.length > 1 ? engines[1] : false);
		}
	}

	getEngineName() {
		return this.engineName;
	}

	isEngine(engine) {
		return this.engineName === engine;
	}

	setUseMarkdown(useMarkdown) {
		this.useMarkdown = !!useMarkdown;
	}

	// this is only called for templateEngineOverride
	setMarkdownEngine(markdownEngine) {
		this.parseMarkdownWith = markdownEngine;
	}

	// this is only called for templateEngineOverride
	setHtmlEngine(htmlEngineName) {
		this.parseHtmlWith = htmlEngineName;
	}

	async _testRender(str, data) {
		return this.engine._testRender(str, data);
	}

	async getCompiledTemplate(str) {
		// TODO refactor better, move into TemplateEngine logic
		if (this.engineName === "md") {
			return this.engine.compile(
				str,
				this.engineNameOrPath,
				this.parseMarkdownWith,
				!this.useMarkdown,
			);
		} else if (this.engineName === "html") {
			return this.engine.compile(str, this.engineNameOrPath, this.parseHtmlWith);
		} else {
			return this.engine.compile(str, this.engineNameOrPath);
		}
	}
}

export default TemplateRender;
