"use strict";

const DocUtils = require("./doc-utils");
DocUtils.traits = require("./traits");
DocUtils.moduleWrapper = require("./module-wrapper");
const {
	throwMultiError,
	throwResolveBeforeCompile,
	throwRenderInvalidTemplate,
} = require("./errors");

const collectContentTypes = require("./collect-content-types");
const ctXML = "[Content_Types].xml";
const commonModule = require("./modules/common");

const Lexer = require("./lexer");
const {
	defaults,
	str2xml,
	xml2str,
	moduleWrapper,
	utf8ToWord,
	concatArrays,
	unique,
} = DocUtils;
const {
	XTInternalError,
	throwFileTypeNotIdentified,
	throwFileTypeNotHandled,
	throwApiVersionError,
} = require("./errors");

const currentModuleApiVersion = [3, 24, 0];

const Docxtemplater = class Docxtemplater {
	constructor(zip, { modules = [], ...options } = {}) {
		if (!Array.isArray(modules)) {
			throw new Error(
				"The modules argument of docxtemplater's constructor must be an array"
			);
		}
		this.compiled = {};
		this.modules = [commonModule()];
		this.setOptions(options);
		modules.forEach((module) => {
			this.attachModule(module);
		});
		if (zip) {
			if (!zip.files || typeof zip.file !== "function") {
				throw new Error(
					"The first argument of docxtemplater's constructor must be a valid zip file (jszip v2 or pizzip v3)"
				);
			}
			this.loadZip(zip);
			// remove the unsupported modules
			this.modules = this.modules.filter((module) => {
				if (module.supportedFileTypes) {
					if (!Array.isArray(module.supportedFileTypes)) {
						throw new Error(
							"The supportedFileTypes field of the module must be an array"
						);
					}
					const isSupportedModule =
						module.supportedFileTypes.indexOf(this.fileType) !== -1;
					if (!isSupportedModule) {
						module.on("detached");
					}
					return isSupportedModule;
				}
				return true;
			});
			this.compile();
			this.v4Constructor = true;
		}
	}
	getModuleApiVersion() {
		return currentModuleApiVersion.join(".");
	}
	verifyApiVersion(neededVersion) {
		neededVersion = neededVersion.split(".").map(function (i) {
			return parseInt(i, 10);
		});
		if (neededVersion.length !== 3) {
			throwApiVersionError("neededVersion is not a valid version", {
				neededVersion,
				explanation: "the neededVersion must be an array of length 3",
			});
		}
		if (neededVersion[0] !== currentModuleApiVersion[0]) {
			throwApiVersionError(
				"The major api version do not match, you probably have to update docxtemplater with npm install --save docxtemplater",
				{
					neededVersion,
					currentModuleApiVersion,
					explanation: `moduleAPIVersionMismatch : needed=${neededVersion.join(
						"."
					)}, current=${currentModuleApiVersion.join(".")}`,
				}
			);
		}
		if (neededVersion[1] > currentModuleApiVersion[1]) {
			throwApiVersionError(
				"The minor api version is not uptodate, you probably have to update docxtemplater with npm install --save docxtemplater",
				{
					neededVersion,
					currentModuleApiVersion,
					explanation: `moduleAPIVersionMismatch : needed=${neededVersion.join(
						"."
					)}, current=${currentModuleApiVersion.join(".")}`,
				}
			);
		}
		if (
			neededVersion[1] === currentModuleApiVersion[1] &&
			neededVersion[2] > currentModuleApiVersion[2]
		) {
			throwApiVersionError(
				"The patch api version is not uptodate, you probably have to update docxtemplater with npm install --save docxtemplater",
				{
					neededVersion,
					currentModuleApiVersion,
					explanation: `moduleAPIVersionMismatch : needed=${neededVersion.join(
						"."
					)}, current=${currentModuleApiVersion.join(".")}`,
				}
			);
		}
		return true;
	}
	setModules(obj) {
		this.modules.forEach((module) => {
			module.set(obj);
		});
	}
	sendEvent(eventName) {
		this.modules.forEach((module) => {
			module.on(eventName);
		});
	}
	attachModule(module, options = {}) {
		if (this.v4Constructor) {
			throw new Error(
				"attachModule() should not be called manually when using the v4 constructor"
			);
		}
		if (module.requiredAPIVersion) {
			this.verifyApiVersion(module.requiredAPIVersion);
		}
		if (module.attached === true) {
			throw new Error(
				`Cannot attach a module that was already attached : "${module.name}". Maybe you are instantiating the module at the root level, and using it for multiple instances of Docxtemplater`
			);
		}
		module.attached = true;
		const { prefix } = options;
		if (prefix) {
			module.prefix = prefix;
		}
		const wrappedModule = moduleWrapper(module);
		this.modules.push(wrappedModule);
		wrappedModule.on("attached");
		return this;
	}
	setOptions(options) {
		if (this.v4Constructor) {
			throw new Error(
				"setOptions() should not be called manually when using the v4 constructor"
			);
		}
		if (!options) {
			throw new Error(
				"setOptions should be called with an object as first parameter"
			);
		}
		if (options.delimiters) {
			options.delimiters.start = utf8ToWord(options.delimiters.start);
			options.delimiters.end = utf8ToWord(options.delimiters.end);
		}
		this.options = {};
		Object.keys(defaults).forEach((key) => {
			const defaultValue = defaults[key];
			this.options[key] = options[key] != null ? options[key] : defaultValue;
			this[key] = this.options[key];
		});
		if (this.zip) {
			this.updateFileTypeConfig();
		}
		return this;
	}
	loadZip(zip) {
		if (zip.loadAsync) {
			throw new XTInternalError(
				"Docxtemplater doesn't handle JSZip version >=3, please use pizzip"
			);
		}
		this.zip = zip;
		this.updateFileTypeConfig();

		this.modules = concatArrays([
			this.fileTypeConfig.baseModules.map(function (moduleFunction) {
				return moduleFunction();
			}),
			this.modules,
		]);
		return this;
	}
	compileFile(fileName) {
		this.compiled[fileName].parse();
	}
	precompileFile(fileName) {
		const currentFile = this.createTemplateClass(fileName);
		currentFile.preparse();
		this.compiled[fileName] = currentFile;
	}
	resolveData(data) {
		let errors = [];
		if (!Object.keys(this.compiled).length) {
			throwResolveBeforeCompile();
		}
		return Promise.resolve(data)
			.then((data) => {
				return Promise.all(
					Object.keys(this.compiled).map((from) => {
						const currentFile = this.compiled[from];
						return currentFile.resolveTags(data).catch(function (errs) {
							errors = errors.concat(errs);
						});
					})
				);
			})
			.then((resolved) => {
				if (errors.length !== 0) {
					throwMultiError(errors);
				}
				return concatArrays(resolved);
			});
	}
	compile() {
		if (Object.keys(this.compiled).length) {
			return this;
		}
		this.options = this.modules.reduce((options, module) => {
			return module.optionsTransformer(options, this);
		}, this.options);
		this.options.xmlFileNames = unique(this.options.xmlFileNames);
		this.xmlDocuments = this.options.xmlFileNames.reduce(
			(xmlDocuments, fileName) => {
				const content = this.zip.files[fileName].asText();
				xmlDocuments[fileName] = str2xml(content);
				return xmlDocuments;
			},
			{}
		);
		this.setModules({
			zip: this.zip,
			xmlDocuments: this.xmlDocuments,
		});
		this.getTemplatedFiles();
		this.setModules({ compiled: this.compiled });
		// Loop inside all templatedFiles (ie xml files with content).
		// Sometimes they don't exist (footer.xml for example)
		this.templatedFiles.forEach((fileName) => {
			if (this.zip.files[fileName] != null) {
				this.precompileFile(fileName);
			}
		});
		this.templatedFiles.forEach((fileName) => {
			if (this.zip.files[fileName] != null) {
				this.compileFile(fileName);
			}
		});
		verifyErrors(this);
		return this;
	}
	updateFileTypeConfig() {
		let fileType;
		if (this.zip.files.mimetype) {
			fileType = "odt";
		}
		const contentTypes = this.zip.files[ctXML];
		this.targets = [];
		const contentTypeXml = contentTypes ? str2xml(contentTypes.asText()) : null;
		const overrides = contentTypeXml
			? contentTypeXml.getElementsByTagName("Override")
			: null;
		const defaults = contentTypeXml
			? contentTypeXml.getElementsByTagName("Default")
			: null;
		if (contentTypeXml) {
			this.filesContentTypes = collectContentTypes(
				overrides,
				defaults,
				this.zip
			);
			this.invertedContentTypes = DocUtils.invertMap(this.filesContentTypes);
			this.setModules({
				contentTypes: this.contentTypes,
				invertedContentTypes: this.invertedContentTypes,
			});
		}
		this.modules.forEach((module) => {
			fileType =
				module.getFileType({
					zip: this.zip,
					contentTypes,
					contentTypeXml,
					overrides,
					defaults,
					doc: this,
				}) || fileType;
		});
		if (fileType === "odt") {
			throwFileTypeNotHandled(fileType);
		}
		if (!fileType) {
			throwFileTypeNotIdentified();
		}

		this.fileType = fileType;

		this.fileTypeConfig =
			this.options.fileTypeConfig ||
			this.fileTypeConfig ||
			Docxtemplater.FileTypeConfig[this.fileType];
		return this;
	}
	render() {
		this.compile();
		if (this.errors.length > 0) {
			throwRenderInvalidTemplate();
		}
		this.setModules({
			data: this.data,
			Lexer,
		});
		this.mapper = this.modules.reduce(function (value, module) {
			return module.getRenderedMap(value);
		}, {});

		this.fileTypeConfig.tagsXmlLexedArray = unique(
			this.fileTypeConfig.tagsXmlLexedArray
		);
		this.fileTypeConfig.tagsXmlTextArray = unique(
			this.fileTypeConfig.tagsXmlTextArray
		);

		Object.keys(this.mapper).forEach((to) => {
			const { from, data } = this.mapper[to];
			const currentFile = this.compiled[from];
			currentFile.setTags(data);
			currentFile.render(to);
			this.zip.file(to, currentFile.content, { createFolders: true });
		});

		verifyErrors(this);
		this.sendEvent("syncing-zip");
		this.syncZip();
		return this;
	}
	syncZip() {
		Object.keys(this.xmlDocuments).forEach((fileName) => {
			this.zip.remove(fileName);
			const content = xml2str(this.xmlDocuments[fileName]);
			return this.zip.file(fileName, content, { createFolders: true });
		});
	}
	setData(data) {
		this.data = data;
		return this;
	}
	getZip() {
		return this.zip;
	}
	createTemplateClass(path) {
		const content = this.zip.files[path].asText();
		return this.createTemplateClassFromContent(content, path);
	}
	createTemplateClassFromContent(content, filePath) {
		const xmltOptions = {
			filePath,
			contentType: this.filesContentTypes[filePath],
		};
		Object.keys(defaults)
			.concat(["filesContentTypes", "fileTypeConfig", "modules"])
			.forEach((key) => {
				xmltOptions[key] = this[key];
			});
		return new Docxtemplater.XmlTemplater(content, xmltOptions);
	}
	getFullText(path) {
		return this.createTemplateClass(
			path || this.fileTypeConfig.textPath(this)
		).getFullText();
	}
	getTemplatedFiles() {
		this.templatedFiles = this.fileTypeConfig.getTemplatedFiles(this.zip);
		this.targets.forEach((target) => {
			this.templatedFiles.push(target);
		});
		return this.templatedFiles;
	}
};

function verifyErrors(doc) {
	const compiled = doc.compiled;
	let allErrors = [];
	Object.keys(compiled).forEach((name) => {
		const templatePart = compiled[name];
		allErrors = concatArrays([allErrors, templatePart.allErrors]);
	});
	doc.errors = allErrors;

	if (allErrors.length !== 0) {
		throwMultiError(allErrors);
	}
}

Docxtemplater.DocUtils = DocUtils;
Docxtemplater.Errors = require("./errors");
Docxtemplater.XmlTemplater = require("./xml-templater");
Docxtemplater.FileTypeConfig = require("./file-type-config");
Docxtemplater.XmlMatcher = require("./xml-matcher");
module.exports = Docxtemplater;
