#include <pch.hpp>
#include "storage.h"
#include "fileSink.h"
#include <commonHelpers.h>
#include <fcntl.h>
#include <logger.h>
#include <boost/filesystem/operations.hpp>
#include <boost/uuid/uuid_generators.hpp>
#include <boost/uuid/uuid_io.hpp>
#include <boost/lexical_cast.hpp>

namespace fs = boost::filesystem;

fs::path Storage::root;
fs::path Storage::byAll;
fs::path Storage::byTitle;
fs::path Storage::byDate;
fs::path Storage::byService;
fs::path Storage::bySchedule;

DECLARE_OPTIONS(Storage, "P2PVR Storage options")
("p2pvr.storage.root", Options::value(&root, "recordings"),
 "Root folder in which to store recordings")
("p2pvr.storage.all", Options::value(&byAll, "all"),
 "Sub folder in which to store all recordings")
("p2pvr.storage.bytitle", Options::value(&byTitle, "title"),
 "Sub folder in which to store by title")
("p2pvr.storage.bydate", Options::value(&byDate, "date"),
 "Sub folder in which to store by date")
("p2pvr.storage.byservice", Options::value(&byService, "service"),
 "Sub folder in which to store by service")
("p2pvr.storage.byschedule", Options::value(&bySchedule, "schedule"),
 "Sub folder in which to store by schedule")
END_OPTIONS(Storage);

inline static
fs::path createPath(const fs::path & start)
{
	return start;
}

template <typename Arg>
inline static
fs::path
operator/(const fs::path & lhs, const IceUtil::Optional<Arg> & rhs)
{
	if (rhs) {
		return lhs / *rhs;
	}
	return lhs;
}

template <typename Arg, typename ... Args>
inline static
fs::path createPath(const fs::path & start, const Arg & arg, const Args & ... args)
{
	auto path = start / arg;
	return createPath(path, args...);
}

template <typename Arg, typename ... Args>
inline static
std::string firstNotNull(const Arg & arg, const Args & ...)
{
	return boost::lexical_cast<std::string>(arg);
}

template <typename Arg, typename ... Args>
inline static
std::string firstNotNull(const IceUtil::Optional<Arg> & arg, const Args & ... args)
{
	return arg ? firstNotNull(*arg, args...) : firstNotNull(args...);
}

template <typename ... Args>
inline static
std::string firstNotNull(const std::string & arg, const Args & ...)
{
	return arg;
}

inline static
std::string
formatIf(const boost::format & f)
{
	return f.str();
}

template <typename Arg, typename ... Args>
inline static
IceUtil::Optional<std::string>
formatIf(boost::format & f, const IceUtil::Optional<Arg> & arg, const Args & ... args)
{
	if (arg) {
		f % *arg;
		return formatIf(f, args...);
	}
	else {
		return IceUtil::Optional<std::string>();
	}
}

template <typename Arg, typename ... Args>
inline static
IceUtil::Optional<std::string>
formatIf(boost::format & f, const Arg & arg, const Args & ... args)
{
	f % arg;
	return formatIf(f, args...);
}

template <typename... Args>
inline static
IceUtil::Optional<std::string>
formatIf(const std::string & msgfmt, const Args & ... args)
{
	boost::format fmt(msgfmt);
	return formatIf(fmt, args...);
}

std::string
Storage::CreateForEventRecording(const std::string & ext, const P2PVR::SchedulePtr & schedule, const DVBSI::ServicePtr & service, const DVBSI::EventPtr & event, const Ice::Current &)
{
	fs::create_directories(root / byAll);
	auto id = boost::lexical_cast<std::string>(boost::uuids::random_generator()());
	fs::path path = root / byAll / id;
	path.replace_extension(ext);
	auto fd = open(path.string().c_str(), O_WRONLY | O_CREAT | O_EXCL, 0664);
	if (fd < 0) {
		Logger()->messagebf(LOG_ERR, "%s: Failed to open file for writing at %s (%d:%s)", __PRETTY_FUNCTION__,
				path, errno, strerror(errno));
		throw P2PVR::StorageException(path.string(), strerror(errno));
	}
	createSymlinks(ext, id, schedule, service, event);
	Logger()->messagebf(LOG_INFO, "%s: Created new recording %s", __PRETTY_FUNCTION__, path);

	close(fd);
	return id;
}

void
Storage::createSymlinks(const std::string & ext, const std::string & target, const P2PVR::SchedulePtr & schedule, const DVBSI::ServicePtr & service, const DVBSI::EventPtr & event)
{
	// By title, with optional season and episode information
	createSymlink(ext, target, createPath(root, byTitle,
				event->Title,
				formatIf("Season %02d", event->Season),
				firstNotNull(
					formatIf("Part %02d of %02d - %s", event->Episode, event->Episodes, event->Subtitle),
					formatIf("Episode %02d - %s", event->Episode, event->Subtitle),
					formatIf("Part %02d of %02d - %s", event->Episode, event->Episodes, event->Description),
					formatIf("Part %02d of %02d", event->Episode, event->Episodes),
					formatIf("Episode %02d - %s", event->Episode, event->Description),
					event->Subtitle,
					event->Description,
					formatIf("Episode %02d", event->Episode),
					event->StartTime)));
	// By date
	createSymlink(ext, target, createPath(root, byDate,
				formatIf("%04d-%02d-%02d", event->StartTime.Year, event->StartTime.Month, event->StartTime.Day),
				firstNotNull(
					formatIf("%s - %s", event->Title, event->Subtitle),
					formatIf("%s - %s", event->Title, event->Description),
					event->Title)));
	// By service
	createSymlink(ext, target, createPath(root, byService,
				service->Name,
				firstNotNull(
					formatIf("%s - %s", event->Title, event->Subtitle),
					formatIf("%s - %s", event->Title, event->Description),
					event->Title)));
	// By schedule title
	createSymlink(ext, target, createPath(root, bySchedule,
				formatIf("Title: %s", schedule->Title),
				formatIf("%04d-%02d-%02d", event->StartTime.Year, event->StartTime.Month, event->StartTime.Day),
				firstNotNull(
					formatIf("%s - %s", event->Title, event->Subtitle),
					formatIf("%s - %s", event->Title, event->Description),
					event->Title)));
	// By schedule search
	createSymlink(ext, target, createPath(root, bySchedule,
				formatIf("Search: %s", schedule->Search),
				firstNotNull(
					formatIf("%s - %s", event->Title, event->Subtitle),
					formatIf("%s - %s", event->Title, event->Description),
					event->Title)));
}
void
Storage::createSymlink(const std::string & ext, const std::string & target, const fs::path & link)
{
	fs::path path = link;
	path.replace_extension(ext);
	Logger()->messagebf(LOG_DEBUG, "%s: link(%s) -> target(%s)", __PRETTY_FUNCTION__, path, target);
	if (fs::exists(path)) {
		Logger()->messagebf(LOG_WARNING, "%s: symlink already exists %s", __PRETTY_FUNCTION__, path);
		return;
	}
	fs::create_directories(path.parent_path());
	fs::path relativeTarget;
	for (fs::path tmp = path.parent_path(); tmp != root; tmp = tmp.parent_path()) {
		relativeTarget /= "..";
	}
	relativeTarget /= byAll;
	relativeTarget /= target;
	relativeTarget.replace_extension(ext);
	fs::create_symlink(relativeTarget, path);
}

std::string
Storage::FindExtension(const std::string & id)
{
	fs::directory_iterator end;
	for (fs::directory_iterator itr(root / byAll); itr != end; ++itr) {
		if (itr->path().stem() == id) {
			return itr->path().extension().string();
		}
	}
	return "";
}

P2PVR::RawDataClientPrx
Storage::OpenForWrite(const std::string & id, const Ice::Current & ice)
{
	fs::path path = root / byAll / id;
	path.replace_extension(FindExtension(id));
	auto fd = open(path.string().c_str(), O_WRONLY | O_APPEND | O_LARGEFILE);
	if (fd < 0) {
		Logger()->messagebf(LOG_ERR, "%s: Failed to open file for reading at %s (%d:%s)", __PRETTY_FUNCTION__,
				path, errno, strerror(errno));
		throw P2PVR::StorageException(path.string(), strerror(errno));
	}
	auto openFile = OpenFilePtr(new OpenFile(ice.adapter, new FileSink(fd)));
	openFiles.insert(openFile);
	return *openFile;
}

void
Storage::Close(const P2PVR::RawDataClientPrx & file, const Ice::Current &)
{
	openFiles.erase(std::find_if(openFiles.begin(), openFiles.end(), [&file](const OpenFilePtr & of) { return *of == file; }));
}

void
Storage::Delete(const std::string & id, const Ice::Current &)
{
	fs::path path = root / byAll / id;
	path.replace_extension(FindExtension(id));
	Logger()->messagebf(LOG_INFO, "%s: Deleting links to %s", __PRETTY_FUNCTION__, path);
	DeleteFrom(path, fs::canonical(root));
}

void
Storage::DeleteFrom(const fs::path & path, const fs::path & from)
{
	Logger()->messagebf(LOG_DEBUG, "%s: Deleting links to %s to %s", __PRETTY_FUNCTION__, path, from);
	fs::directory_iterator end;
	for (fs::directory_iterator itr(from); itr != end; ++itr) {
		if (fs::is_directory(*itr)) {
			DeleteFrom(path, *itr);
		}
		else {
			boost::system::error_code err;
			auto link = fs::canonical(*itr, err);
			if (err || link == path) {
				Logger()->messagebf(LOG_DEBUG, "%s: deleting %s", __PRETTY_FUNCTION__, *itr);
				fs::remove(*itr);
			}
		}
	}
	if (from != root && fs::is_empty(from)) {
		Logger()->messagebf(LOG_DEBUG, "%s: deleting directory %s", __PRETTY_FUNCTION__, from);
		fs::remove(from);
	}
}