"use strict";

const path = require("path");
const defaultValue = require("default-value");
const createEventEmitter = require("create-event-emitter");

const mergeMetadata = require("./util/merge-metadata");

module.exports = function pipeline(steps) {
	if (steps.length < 2) {
		throw new Error("A pipeline must consist of at least two steps");
	} else if (steps[0].__buildStep !== "source") {
		throw new Error("The first transform in a pipeline must be a file source");
	} else if (steps[steps.length - 1].__buildStep !== "sink") {
		throw new Error("The last transform in a pipeline must be a file sink");
	} else {
		let basePath = steps[0].basePath;
		let sources = steps.slice(0, -1);
		let sinks = steps.slice(1);
		let firstSource = sources[0];
		let finalSink = sinks[sinks.length - 1];

		let emitter = createEventEmitter({
			basePath: basePath, /* QUESTION: Should sources be required to provide this? */
			steps: steps, /* FIXME: Make private? */
			initialize: function () {
				let allSinksSupportStreams = sinks.every((sink) => sink.supportsStreams === true);

				/* This will start the flow. */
				firstSource.initialize({
					supportsStreams: allSinksSupportStreams
				});
			}
		});

		steps.forEach((step) => {
			step.on("error", (data) => {
				if (data instanceof Error) {
					/* FIXME: This is because any error within an event handler will automatically result in an `error` event. Probably should rename the transform error event to avoid interfering with this. */
					process.nextTick(() => {
						throw data;
					});
				} else {
					let error = Object.assign({
						isFatal: true
					}, data);

					emitter.emit("error", error);
				}
			});
		});

		sources.forEach((source, i) => {
			source.on("file", ({metadata, contents, stream}) => {
				/* FIXME: Add stream-to-contents conversion? */
				let augmentedMetadata;

				if (metadata.isVirtual === false && metadata.relativePath == null) {
					/* FIXME: Add check that 1) absolute path is set, and 2) it lies within the basePath */
					augmentedMetadata = mergeMetadata(metadata, {
						relativePath: path.relative(basePath, metadata.path)
					});
				} else {
					augmentedMetadata = metadata;
				}

				augmentedMetadata.basePath = basePath;

				/* NOTE: The below indexes into `steps`, not `sources`, because the last source sends to the final sink. Both `steps` and `sources` arrays start at the same point. */
				let nextSink = steps[i + 1];

				try {
					let sinkInput = {
						metadata: augmentedMetadata,
						contents: contents,
						stream: stream
					};

					if (nextSink.__buildStep === "transform") {
						nextSink.transform(sinkInput);
					} else if (nextSink.__buildStep === "sink") {
						nextSink.sink(sinkInput);
					}
				} catch (error) {
					emitter.emit("error", {
						step: nextSink,
						file: augmentedMetadata,
						error: error,
						isFatal: defaultValue(error.isFatal, true)
					});
				}
			});
		});

		finalSink.on("sunk", ({metadata}) => {
			emitter.emit("done", {
				metadata: metadata,
				acknowledged: finalSink.supportsAcknowledgment
			});
		});

		return emitter;
	}
};
