#include "dv_input.hpp"

#include <boost/nowide/fstream.hpp>

#include <algorithm>
#include <thread>
#include <utility>

class InFile : public dv::ModuleBase {
private:
	static std::pair<boost::nowide::ifstream, dv::InputInformation> regenerateOutputs(
		const std::string &path, dvModuleData mData, dv::Config::Node mNode) {
		// Gather current outputs and types.
		std::vector<std::pair<std::string, std::string>> oldOutputs;

		for (auto &child : mNode.getRelativeNode("outputs/").getChildren()) {
			oldOutputs.emplace_back(child.getName(), child.getString("typeIdentifier"));
		}

		dv::vectorSortUnique(oldOutputs);

		// Open and parse file.
		boost::nowide::ifstream fileStream = dv::InputDecoder::openFile(path);

		// Re-generate outputs list from currently open file.
		dv::InputInformation fileInfo = dv::InputDecoder::parseHeader(fileStream, mNode, true);

		// Register outputs with module IO system, resolve type references.
		std::vector<std::pair<std::string, std::string>> newOutputs;

		// Get all the new outputs that have to exist after we complete this operation.
		for (auto &stream : fileInfo.streams) {
			newOutputs.emplace_back(stream.name, stream.typeIdentifier);
		}

		dv::vectorSortUnique(newOutputs);

		// Diff the old and new outputs: the ones that remains the same are not touched,
		// new ones are added, and old ones are removed.
		std::vector<std::pair<std::string, std::string>> outputsToAdd;

		std::set_difference(newOutputs.begin(), newOutputs.end(), oldOutputs.begin(), oldOutputs.end(),
			std::inserter(outputsToAdd, outputsToAdd.begin()));

		for (auto &out : outputsToAdd) {
			dvModuleRegisterOutput(mData, out.first.c_str(), ("INPUT_" + out.second).c_str());
		}

		std::vector<std::pair<std::string, std::string>> outputsToRemove;

		std::set_difference(oldOutputs.begin(), oldOutputs.end(), newOutputs.begin(), newOutputs.end(),
			std::inserter(outputsToRemove, outputsToRemove.begin()));

		for (auto &out : outputsToRemove) {
			dvModuleRegisterOutput(mData, out.first.c_str(), "REMOVE");
		}

		return (std::make_pair(std::move(fileStream), std::move(fileInfo)));
	}

	static void fileChangeListener(dvConfigNode node, void *userData, enum dvConfigAttributeEvents event,
		const char *changeKey, enum dvConfigAttributeType changeType, union dvConfigAttributeValue changeValue) {
		auto mData = static_cast<dvModuleData>(userData);
		auto mNode = dv::Cfg::Node(node);

		if ((event == DVCFG_ATTRIBUTE_MODIFIED) && (changeType == DVCFG_TYPE_STRING)
			&& (std::string{changeKey} == "file")) {
			auto moduleRunning = (mNode.getBool("isRunning") || mNode.getBool("running"));

			if (moduleRunning) {
				// Stop module. Change to 'isRunning' will continue file parsing.
				mNode.putBool("running", false);
			}

			// Reset to default values (like when just created) on file change.
			mNode.putLong("seek", 0);
			mNode.putLong("seekStart", 0);
			mNode.create<dv::CfgType::LONG>(
				"seekEnd", INT64_MAX, {0, INT64_MAX}, dv::CfgFlags::NORMAL, "End playback point.");
			mNode.putLong("seekEnd", INT64_MAX);

			if (!moduleRunning) {
				try {
					// Module not running, let's update the outputs.
					regenerateOutputs(changeValue.string, mData, mNode);
				}
				catch (const std::exception &ex) {
					dv::Log(dv::logLevel::ERROR, "File '{:s}': {:s}", changeValue.string, ex.what());
				}
			}
		}
	}

	static void isRunningChangeListener(dvConfigNode node, void *userData, enum dvConfigAttributeEvents event,
		const char *changeKey, enum dvConfigAttributeType changeType, union dvConfigAttributeValue changeValue) {
		auto mData = static_cast<dvModuleData>(userData);
		auto mNode = dv::Cfg::Node(node);

		if ((event == DVCFG_ATTRIBUTE_MODIFIED) && (changeType == DVCFG_TYPE_BOOL)
			&& (std::string{changeKey} == "isRunning") && (!changeValue.boolean)) {
			try {
				regenerateOutputs(mNode.getString("file"), mData, node);
			}
			catch (const std::exception &ex) {
				dv::Log(dv::logLevel::ERROR, "File '{:s}': {:s}", mNode.getString("file"), ex.what());
			}
		}
	}

	boost::nowide::ifstream fileStream;
	dv::InputInformation fileInfo;
	dv::InputDecoder fileDecoder;
	bool hitEOF;
	int64_t lastSeek;
	bool firstPause;

public:
	static const char *initDescription() {
		return ("Read AEDAT 4.0 data from a file.");
	}

	static void initConfigOptions(dv::RuntimeConfig &config) {
		// First character of AEDAT4_FILE_EXTENSION is a ".", we need to omit it, so substr called is used.
		config.add("file", dv::ConfigOption::fileOpenOption("File to load data from.",
							   std::string(dv::io::support::AEDAT4_FILE_EXTENSION).substr(1)));
		config.add("mode", dv::ConfigOption::listOption("Time or event-based readout.", 0, {"timeInterval"}));
		// TODO: re-add "eventNumber" mode later.

		config.add(
			"timeInterval", dv::ConfigOption::longOption(
								"Time interval to extract for each packet of data (in µs).", 10000, 1, 10000000));
		// config.add("eventNumber",
		// dv::ConfigOption::longOption("Number of elements to extract for each packet of data.", 2000));

		config.add(
			"timeDelay", dv::ConfigOption::longOption("Time between consecutive packets (in µs).", 10000, 0, 10000000));

		config.add("pause", dv::ConfigOption::boolOption("Pause playback.", false));
		config.add("loop", dv::ConfigOption::boolOption("Loop playback.", false));
		auto seek = dv::ConfigOption::longOption("Current playback point.", 0, 0, INT64_MAX);
		seek.setRateLimit(1, 250);
		config.add("seek", std::move(seek));
		config.add("seekStart", dv::ConfigOption::longOption("Start playback point.", 0, 0, INT64_MAX));
		config.add("seekEnd", dv::ConfigOption::longOption("End playback point.", INT64_MAX, 0, INT64_MAX));

		config.setPriorityOptions({"file", "loop"});
	}

	static void advancedStaticInit(dvModuleData mData) {
		auto mNode = dv::Cfg::Node(mData->moduleNode);

		// Add read-only fileSize.
		mNode.create<dv::CfgType::LONG>(
			"fileSize", 0, {0, INT64_MAX}, dv::CfgFlags::READ_ONLY | dv::CfgFlags::NO_EXPORT, "File size in bytes.");

		// Add default outputs if no file is selected.
		if (mNode.getString("file").empty()) {
			dvModuleRegisterOutput(mData, "events", dv::EventPacket::TableType::identifier);
			dvModuleRegisterOutput(mData, "frames", dv::Frame::TableType::identifier);
			dvModuleRegisterOutput(mData, "imu", dv::IMUPacket::TableType::identifier);
			dvModuleRegisterOutput(mData, "triggers", dv::TriggerPacket::TableType::identifier);
		}
		else {
			// Ensure outputs are generated at startup.
			try {
				regenerateOutputs(mNode.getString("file"), mData, mNode);
			}
			catch (const std::exception &ex) {
				dv::Log(dv::logLevel::ERROR, "File '{:s}': {:s}", mNode.getString("file"), ex.what());
			}
		}

		// On changes to 'files' attribute, re-generate the list of inputs,
		// or shut down the module, to allow user to start with new selection.
		mNode.addAttributeListener(mData, &fileChangeListener);

		// On changes to 'isRunning = false', regenerate the outputs.
		mNode.addAttributeListener(mData, &isRunningChangeListener);
	}

	InFile() : fileDecoder(moduleData, &config, &log), hitEOF(false) {
		try {
			// Open file and regenerate outputs list from currently open file.
			std::tie(fileStream, fileInfo) = regenerateOutputs(config.getString("file"), moduleData, moduleNode);
		}
		catch (const std::exception &ex) {
			throw std::runtime_error(fmt::format(FMT_STRING("File '{:s}': {:s}"), config.getString("file"), ex.what()));
		}

		// Check if there are any viable streams in this file.
		if (fileInfo.streams.empty()) {
			throw std::runtime_error(
				fmt::format(FMT_STRING("File '{:s}': no valid streams found."), config.getString("file")));
		}

		// Register outputs with module IO system, resolve type references.
		for (auto &st : fileInfo.streams) {
			st.type = dvTypeSystemGetInfoByIdentifier(st.typeIdentifier.c_str());
		}

		// Setup decoder once all info is there.
		fileDecoder.setup(&fileStream, &fileInfo);

		// Log details.
		log.debug.format("Compression: {:s}, data table position: {:d}.",
			dv::EnumNameCompressionType(fileInfo.compression), fileInfo.dataTablePosition);

		if (fileInfo.dataTable->Table.empty()) {
			throw std::runtime_error(
				fmt::format(FMT_STRING("File '{:s}': no data table found, corrupt file."), config.getString("file")));
		}

		// Check seekStart/End.
		if (config.getLong("seekStart") > config.getLong("seekEnd")) {
			throw std::runtime_error("seekStart is after seekEnd, please fix your configuration!");
		}

		// Setup seek, start is from seekStart always.
		config.setLong("seek", config.getLong("seekStart"));
		lastSeek   = config.getLong("seek");
		firstPause = true;

		// Done.
		log.info << "Opened file '" << config.getString("file") << "' successfully." << std::endl;
	}

	~InFile() override {
		// Looping support: at the end of file, we shut down, always,
		// to simplify the module. And so to loop we simply automatically
		// start running again.
		if (hitEOF && config.getBool("loop")) {
			config.setBool("running", true);

			log.info << "Loop mode set, starting playback again." << std::endl;
		}

		// Disable pause on shutdown.
		config.setBool("pause", false);

		// Go back to beginning of file on next start.
		config.setLong("seek", 0);
	}

	void run() override {
		// Check seekStart/End.
		if (config.getLong("seekStart") > config.getLong("seekEnd")) {
			log.error << "seekStart is after seekEnd, please fix your configuration!" << std::endl;

			// Do nothing, 1s delay.
			std::this_thread::sleep_for(std::chrono::seconds(1));
			return;
		}

		if (config.getBool("pause")) {
			if (firstPause) {
				// Restore value after configUpdate() has reset it.
				config.set<dv::CfgType::LONG>("seek", lastSeek, true);
				firstPause = false;
				goto play;
			}

			if (lastSeek != config.getLong("seek")) {
				// Seek while paused, send one packet.
				goto play;
			}

			// Do nothing, 50ms delay.
			std::this_thread::sleep_for(std::chrono::milliseconds(50));
			return;
		}
		else {
			firstPause = true;
		}

	play:
		try {
			fileDecoder.run();
		}
		catch (const std::ios::failure &ex) {
			// Unrecoverable IO problem.
			config.setBool("running", false);

			if ((fileStream.eof()) || (strncmp(ex.what(), "EOF", 3) == 0)) {
				// EOF is normal and just shuts the module down.
				log.info << "End of File reached, shutting down." << std::endl;

				// Remember we hit end-of-file for shutdown operation.
				hitEOF = true;
			}
			else {
				// Actual failure, throw up.
				throw;
			}
		}

		lastSeek = config.getLong("seek");
	}
};

registerModuleClass(InFile)
