"use strict";
const { last, first } = require("./utils");

function XTError(message) {
	this.name = "GenericError";
	this.message = message;
	this.stack = new Error(message).stack;
}
XTError.prototype = Error.prototype;

function XTTemplateError(message) {
	this.name = "TemplateError";
	this.message = message;
	this.stack = new Error(message).stack;
}
XTTemplateError.prototype = new XTError();

function XTRenderingError(message) {
	this.name = "RenderingError";
	this.message = message;
	this.stack = new Error(message).stack;
}
XTRenderingError.prototype = new XTError();

function XTScopeParserError(message) {
	this.name = "ScopeParserError";
	this.message = message;
	this.stack = new Error(message).stack;
}
XTScopeParserError.prototype = new XTError();

function XTInternalError(message) {
	this.name = "InternalError";
	this.properties = { explanation: "InternalError" };
	this.message = message;
	this.stack = new Error(message).stack;
}
XTInternalError.prototype = new XTError();

function XTAPIVersionError(message) {
	this.name = "APIVersionError";
	this.properties = { explanation: "APIVersionError" };
	this.message = message;
	this.stack = new Error(message).stack;
}
XTAPIVersionError.prototype = new XTError();

function throwApiVersionError(msg, properties) {
	const err = new XTAPIVersionError(msg);
	err.properties = {
		id: "api_version_error",
		...properties,
	};
	throw err;
}

function throwMultiError(errors) {
	const err = new XTTemplateError("Multi error");
	err.properties = {
		errors,
		id: "multi_error",
		explanation: "The template has multiple errors",
	};
	throw err;
}

function getUnopenedTagException(options) {
	const err = new XTTemplateError("Unopened tag");
	err.properties = {
		xtag: last(options.xtag.split(" ")),
		id: "unopened_tag",
		context: options.xtag,
		offset: options.offset,
		lIndex: options.lIndex,
		explanation: `The tag beginning with "${options.xtag.substr(
			0,
			10
		)}" is unopened`,
	};
	return err;
}

function getDuplicateOpenTagException(options) {
	const err = new XTTemplateError("Duplicate open tag, expected one open tag");
	err.properties = {
		xtag: first(options.xtag.split(" ")),
		id: "duplicate_open_tag",
		context: options.xtag,
		offset: options.offset,
		lIndex: options.lIndex,
		explanation: `The tag beginning with "${options.xtag.substr(
			0,
			10
		)}" has duplicate open tags`,
	};
	return err;
}

function getDuplicateCloseTagException(options) {
	const err = new XTTemplateError(
		"Duplicate close tag, expected one close tag"
	);
	err.properties = {
		xtag: first(options.xtag.split(" ")),
		id: "duplicate_close_tag",
		context: options.xtag,
		offset: options.offset,
		lIndex: options.lIndex,
		explanation: `The tag ending with "${options.xtag.substr(
			0,
			10
		)}" has duplicate close tags`,
	};
	return err;
}

function getUnclosedTagException(options) {
	const err = new XTTemplateError("Unclosed tag");
	err.properties = {
		xtag: first(options.xtag.split(" ")).substr(1),
		id: "unclosed_tag",
		context: options.xtag,
		offset: options.offset,
		lIndex: options.lIndex,
		explanation: `The tag beginning with "${options.xtag.substr(
			0,
			10
		)}" is unclosed`,
	};
	return err;
}

function throwXmlTagNotFound(options) {
	const err = new XTTemplateError(
		`No tag "${options.element}" was found at the ${options.position}`
	);
	const part = options.parsed[options.index];
	err.properties = {
		id: `no_xml_tag_found_at_${options.position}`,
		explanation: `No tag "${options.element}" was found at the ${options.position}`,
		offset: part.offset,
		part,
		parsed: options.parsed,
		index: options.index,
		element: options.element,
	};
	throw err;
}

function getCorruptCharactersException({ tag, value, offset }) {
	const err = new XTRenderingError("There are some XML corrupt characters");
	err.properties = {
		id: "invalid_xml_characters",
		xtag: tag,
		value,
		offset,
		explanation: "There are some corrupt characters for the field ${tag}",
	};
	return err;
}

function throwContentMustBeString(type) {
	const err = new XTInternalError("Content must be a string");
	err.properties.id = "xmltemplater_content_must_be_string";
	err.properties.type = type;
	throw err;
}

function throwExpandNotFound(options) {
	const {
		part: { value, offset },
		id = "raw_tag_outerxml_invalid",
		message = "Raw tag not in paragraph",
	} = options;
	const { part } = options;
	let {
		explanation = `The tag "${value}" is not inside a paragraph`,
	} = options;
	if (typeof explanation === "function") {
		explanation = explanation(part);
	}
	const err = new XTTemplateError(message);
	err.properties = {
		id,
		explanation,
		rootError: options.rootError,
		xtag: value,
		offset,
		postparsed: options.postparsed,
		expandTo: options.expandTo,
		index: options.index,
	};
	throw err;
}

function throwRawTagShouldBeOnlyTextInParagraph(options) {
	const err = new XTTemplateError(
		"Raw tag should be the only text in paragraph"
	);
	const tag = options.part.value;
	err.properties = {
		id: "raw_xml_tag_should_be_only_text_in_paragraph",
		explanation: `The raw tag "${tag}" should be the only text in this paragraph. This means that this tag should not be surrounded by any text or spaces.`,
		xtag: tag,
		offset: options.part.offset,
		paragraphParts: options.paragraphParts,
	};
	throw err;
}

function getUnmatchedLoopException(options) {
	const { location } = options;
	const t = location === "start" ? "unclosed" : "unopened";
	const T = location === "start" ? "Unclosed" : "Unopened";

	const err = new XTTemplateError(`${T} loop`);
	const tag = options.part.value;
	err.properties = {
		id: `${t}_loop`,
		explanation: `The loop with tag "${tag}" is ${t}`,
		xtag: tag,
		offset: options.part.offset,
	};
	return err;
}

function getClosingTagNotMatchOpeningTag({ tags }) {
	const err = new XTTemplateError("Closing tag does not match opening tag");
	err.properties = {
		id: "closing_tag_does_not_match_opening_tag",
		explanation: `The tag "${tags[0].value}" is closed by the tag "${tags[1].value}"`,
		openingtag: first(tags).value,
		offset: [first(tags).offset, last(tags).offset],
		closingtag: last(tags).value,
	};
	return err;
}

function getScopeCompilationError({ tag, rootError, offset }) {
	const err = new XTScopeParserError("Scope parser compilation failed");
	err.properties = {
		id: "scopeparser_compilation_failed",
		offset,
		tag,
		explanation: `The scope parser for the tag "${tag}" failed to compile`,
		rootError,
	};
	return err;
}

function getScopeParserExecutionError({ tag, scope, error, offset }) {
	const err = new XTScopeParserError("Scope parser execution failed");
	err.properties = {
		id: "scopeparser_execution_failed",
		explanation: `The scope parser for the tag ${tag} failed to execute`,
		scope,
		offset,
		tag,
		rootError: error,
	};
	return err;
}

function getLoopPositionProducesInvalidXMLError({ tag, offset }) {
	const err = new XTTemplateError(
		`The position of the loop tags "${tag}" would produce invalid XML`
	);
	err.properties = {
		tag,
		id: "loop_position_invalid",
		explanation: `The tags "${tag}" are misplaced in the document, for example one of them is in a table and the other one outside the table`,
		offset,
	};
	return err;
}

function throwUnimplementedTagType(part, index) {
	let errorMsg = `Unimplemented tag type "${part.type}"`;
	if (part.module) {
		errorMsg += ` "${part.module}"`;
	}
	const err = new XTTemplateError(errorMsg);
	err.properties = {
		part,
		index,
		id: "unimplemented_tag_type",
	};
	throw err;
}

function throwMalformedXml(part) {
	const err = new XTInternalError("Malformed xml");
	err.properties = {
		part,
		id: "malformed_xml",
	};
	throw err;
}

function throwLocationInvalid(part) {
	throw new XTInternalError(
		`Location should be one of "start" or "end" (given : ${part.location})`
	);
}

function throwResolveBeforeCompile() {
	const err = new XTInternalError(
		"You must run `.compile()` before running `.resolveData()`"
	);
	err.properties = {
		id: "resolve_before_compile",
	};
	throw err;
}

function throwRenderInvalidTemplate() {
	const err = new XTInternalError(
		"You should not call .render on a document that had compilation errors"
	);
	err.properties = {
		id: "render_on_invalid_template",
	};
	throw err;
}

function throwFileTypeNotIdentified() {
	const err = new XTInternalError(
		"The filetype for this file could not be identified, is this file corrupted ?"
	);
	err.properties = {
		id: "filetype_not_identified",
	};
	throw err;
}

function throwXmlInvalid(content, offset) {
	const err = new XTTemplateError("An XML file has invalid xml");
	err.properties = {
		id: "file_has_invalid_xml",
		content,
		offset,
		explanation: "The docx contains invalid XML, it is most likely corrupt",
	};
	throw err;
}

function throwFileTypeNotHandled(fileType) {
	const err = new XTInternalError(
		`The filetype "${fileType}" is not handled by docxtemplater`
	);
	err.properties = {
		id: "filetype_not_handled",
		explanation: `The file you are trying to generate is of type "${fileType}", but only docx and pptx formats are handled`,
		fileType,
	};
	throw err;
}

module.exports = {
	XTError,
	XTTemplateError,
	XTInternalError,
	XTScopeParserError,
	XTAPIVersionError,
	// Remove this alias in v4
	RenderingError: XTRenderingError,
	XTRenderingError,

	getClosingTagNotMatchOpeningTag,
	getLoopPositionProducesInvalidXMLError,
	getScopeCompilationError,
	getScopeParserExecutionError,
	getUnclosedTagException,
	getUnopenedTagException,
	getUnmatchedLoopException,
	getDuplicateCloseTagException,
	getDuplicateOpenTagException,
	getCorruptCharactersException,

	throwApiVersionError,
	throwContentMustBeString,
	throwFileTypeNotHandled,
	throwFileTypeNotIdentified,
	throwLocationInvalid,
	throwMalformedXml,
	throwMultiError,
	throwExpandNotFound,
	throwRawTagShouldBeOnlyTextInParagraph,
	throwUnimplementedTagType,
	throwXmlTagNotFound,
	throwXmlInvalid,
	throwResolveBeforeCompile,
	throwRenderInvalidTemplate,
};
