"use strict";
const { getScopeParserExecutionError } = require("./errors");
const { last } = require("./utils");
const { concatArrays } = require("./doc-utils");

function find(list, fn) {
	const length = list.length >>> 0;
	let value;

	for (let i = 0; i < length; i++) {
		value = list[i];
		if (fn.call(this, value, i, list)) {
			return value;
		}
	}
	return undefined;
}

function getValue(tag, meta, num) {
	const scope = this.scopeList[num];
	if (this.resolved) {
		let w = this.resolved;
		this.scopePath.forEach((p, index) => {
			const lIndex = this.scopeLindex[index];
			w = find(w, function (r) {
				return r.lIndex === lIndex;
			});
			w = w.value[this.scopePathItem[index]];
		});
		return [
			this.scopePath.length - 1,
			find(w, function (r) {
				return meta.part.lIndex === r.lIndex;
			}).value,
		];
	}
	// search in the scopes (in reverse order) and keep the first defined value
	let result;

	let parser;
	if (!this.cachedParsers || !meta.part) {
		parser = this.parser(tag, {
			scopePath: this.scopePath,
		});
	} else if (this.cachedParsers[meta.part.lIndex]) {
		parser = this.cachedParsers[meta.part.lIndex];
	} else {
		parser = this.cachedParsers[meta.part.lIndex] = this.parser(tag, {
			scopePath: this.scopePath,
		});
	}
	try {
		result = parser.get(scope, this.getContext(meta, num));
	} catch (error) {
		throw getScopeParserExecutionError({
			tag,
			scope,
			error,
			offset: meta.part.offset,
		});
	}
	if (result == null && num > 0) {
		return getValue.call(this, tag, meta, num - 1);
	}
	return [num, result];
}

function getValueAsync(tag, meta, num) {
	const scope = this.scopeList[num];
	// search in the scopes (in reverse order) and keep the first defined value
	let parser;
	if (!this.cachedParsers || !meta.part) {
		parser = this.parser(tag, {
			scopePath: this.scopePath,
		});
	} else if (this.cachedParsers[meta.part.lIndex]) {
		parser = this.cachedParsers[meta.part.lIndex];
	} else {
		parser = this.cachedParsers[meta.part.lIndex] = this.parser(tag, {
			scopePath: this.scopePath,
		});
	}

	return Promise.resolve()
		.then(() => {
			return parser.get(scope, this.getContext(meta, num));
		})
		.catch(function (error) {
			throw getScopeParserExecutionError({
				tag,
				scope,
				error,
				offset: meta.part.offset,
			});
		})
		.then((result) => {
			if (result == null && num > 0) {
				return getValueAsync.call(this, tag, meta, num - 1);
			}
			return result;
		});
}

// This class responsibility is to manage the scope
const ScopeManager = class ScopeManager {
	constructor(options) {
		this.scopePath = options.scopePath;
		this.scopePathItem = options.scopePathItem;
		this.scopePathLength = options.scopePathLength;
		this.scopeList = options.scopeList;
		this.scopeLindex = options.scopeLindex;
		this.parser = options.parser;
		this.resolved = options.resolved;
		this.cachedParsers = options.cachedParsers;
	}
	loopOver(tag, functor, inverted, meta) {
		return this.loopOverValue(this.getValue(tag, meta), functor, inverted);
	}
	functorIfInverted(inverted, functor, value, i, length) {
		if (inverted) {
			functor(value, i, length);
		}
		return inverted;
	}
	isValueFalsy(value, type) {
		return (
			value == null ||
			!value ||
			(type === "[object Array]" && value.length === 0)
		);
	}
	loopOverValue(value, functor, inverted) {
		if (this.resolved) {
			inverted = false;
		}
		const type = Object.prototype.toString.call(value);
		if (this.isValueFalsy(value, type)) {
			return this.functorIfInverted(
				inverted,
				functor,
				last(this.scopeList),
				0,
				1
			);
		}
		if (type === "[object Array]") {
			for (let i = 0; i < value.length; i++) {
				this.functorIfInverted(!inverted, functor, value[i], i, value.length);
			}
			return true;
		}
		if (type === "[object Object]") {
			return this.functorIfInverted(!inverted, functor, value, 0, 1);
		}
		return this.functorIfInverted(
			!inverted,
			functor,
			last(this.scopeList),
			0,
			1
		);
	}
	getValue(tag, meta) {
		const [num, result] = getValue.call(
			this,
			tag,
			meta,
			this.scopeList.length - 1
		);
		this.num = num;
		return result;
	}
	getValueAsync(tag, meta) {
		return getValueAsync.call(this, tag, meta, this.scopeList.length - 1);
	}
	getContext(meta, num) {
		return {
			num,
			meta,
			scopeList: this.scopeList,
			resolved: this.resolved,
			scopePath: this.scopePath,
			scopePathItem: this.scopePathItem,
			scopePathLength: this.scopePathLength,
		};
	}
	createSubScopeManager(scope, tag, i, part, length) {
		return new ScopeManager({
			resolved: this.resolved,
			parser: this.parser,
			cachedParsers: this.cachedParsers,
			scopeList: concatArrays([this.scopeList, [scope]]),
			scopePath: concatArrays([this.scopePath, [tag]]),
			scopePathItem: concatArrays([this.scopePathItem, [i]]),
			scopePathLength: concatArrays([this.scopePathLength, [length]]),
			scopeLindex: concatArrays([this.scopeLindex, [part.lIndex]]),
		});
	}
};

module.exports = function (options) {
	options.scopePath = [];
	options.scopePathItem = [];
	options.scopePathLength = [];
	options.scopeLindex = [];
	options.scopeList = [options.tags];
	return new ScopeManager(options);
};
