/* Lecture code 17.0
 *
 * GDFReader class illustrating policy classes.  There are two policies here:
 * 1. ErrorPolicy: Must provide a reportError function that responds to errors.
 * 2. ReaderPolicy: Must provide a Reader type that can read from some source.
 *
 * Comments have been added to the appropriate sections.
 */

#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <map>
#include <queue>
#include <algorithm>
#include <iterator>
#include <memory>
#include <stdexcept>
using namespace std;

/* Helper base class that disallows copying. */
class Uncopyable
{
protected:
	Uncopyable()  {}
	~Uncopyable() {}
private:
	Uncopyable(const Uncopyable& other);
	Uncopyable& operator= (const Uncopyable& other);
};

/* Policy class: IgnoreAllErrors
 * Ignores all errors.
 */
class IgnoreAllErrors
{
protected:
	IgnoreAllErrors() {}
	~IgnoreAllErrors() {}
	static void reportError(const string& error)
	{
	}
};

/* Policy class: ThrowOnError
 * Throws an exception of type runtime_error on an error.
 */
class ThrowOnError
{
protected:
	ThrowOnError() {}
	~ThrowOnError() {}
	
	static void reportError(const string& error)
	{
		throw runtime_error(error);
	}
};

/* Policy Class: WriteToDiskOnError
 * Class that logs information to a file if an error occurs.
 */
class WriteToDiskOnError
{
public:
	/* Creates a public interface method called setOutputFile which controls where
	 * errors should be written.
	 */
	void setOutputFile(const string& str)
	{
		output.open(str.c_str());
	}
protected:
	WriteToDiskOnError() {}
	~WriteToDiskOnError() {}

	/* Report error writes errors to the file stream. */
	void reportError(const string& error) const
	{
		output << error << endl;
	}
private:
	/* Output is marked mutable so that const member functions can still log data.
	 * This is primarily because the logging is independent of the rest of the class's
	 * functionality.
	 */
	mutable ofstream output;
};

/* Policy class: IOStreamReader
 * This class is very tricky to read correctly.  It exports a helper type
 * Reader which can read from an ifstream.  It also exports a public function,
 * readFile, which opens a file and then invokes the GDFReader's loadFromSource
 * method.  However, since loadFromSource is declared in the derived class, we use
 * static polymorphism and the CRTP to invoke the derived class method.
 */

/* Template argument is the type which will inherit from us. */
template <typename DerivedClass> class IOStreamReader
{
public:
	void readFile(const string& filename)
	{
		/* Open the input file, fail if we can't. */
		Reader result(filename);

		/* Since loadFromSource is in the derived class, typecast ourself to a
		 * DerivedClass object, then invoke the loadFromSource function.  Provided that
		 * this class is used properly, this is a safe operation.
		 */
		static_cast<DerivedClass*>(this)->loadFromSource(result);
	}
protected:
	IOStreamReader() {}
	~IOStreamReader() {}

	/* Reader class reads from an ifstream. */
	class Reader
	{
	public:
		explicit Reader(const string& filename)
		{
			input.open(filename.c_str());
		}
		bool isOpen() const
		{
			return input.is_open();
		}
		string readLine()
		{
			string result;
			getline(input, result);
			return result;
		}
	private:
		ifstream input;
	};
};

/**
 * A basic implementation of a class that can read, write, and maintain a
 * GDF document.
 */
template <
			typename ErrorPolicy = IgnoreAllErrors,

			/* This next line is notoriously tricky to read.  It states that the argument
			 * to this template is _itself_ a template class.  Thus when instantiating this
			 * template, we will pass in the name of a class template.
			 */
			template <typename DerivedClass> class ReaderPolicy = IOStreamReader
         >
class GDFReader: private Uncopyable, // For simplicity, disallow copy.
				 public ErrorPolicy,
				 /* Using CRTP, we inherit from ReaderPolicy instantiated on this type. */
				 public ReaderPolicy<GDFReader<ErrorPolicy, ReaderPolicy> >
{
	/* For simplicity's sake, typedef the Reader class exported by ReaderPolicy
	 * as InputSource.
	 */
	typedef typename ReaderPolicy<GDFReader<ErrorPolicy, ReaderPolicy> >::Reader InputSource;

	/* For CRTP, the base class ReaderPolicy must be able to read our loadFromSource method. */
	friend class ReaderPolicy<GDFReader<ErrorPolicy, ReaderPolicy> >;
public:
	GDFReader();

	/**
	 * Outputs a GDF document based on the contents of this directory
	 * structure.
	 */
	void writeFile(const string& filename);

	/**
	 * Returns the contents of the specified file.
	 */
	vector<string> getFileContents(const string& filename) const;

	/**
	 * Overwrites the contents of the specified file with the specified
	 * contents.
	 */
	void setFileContents(const string& filename, const vector<string>& contents);

	/**
	 * Creates a new file.  The containing directory must exist.
	 */
	void addFile(const string& filename, const vector<string>& contents = vector<string>());

	/**
	 * Creates a new directory.  The containing directory must exist.
	 */
	void addDirectory(const string& filename);

	/**
	 * Empties out the directory structure.
	 */
	void clear();
private:

	struct directoryT
	{
		map<string, vector<string> > allFiles;
		map<string, directoryT* > allDirectories;

		~directoryT()
		{
			for(map<string, directoryT*>::iterator itr = allDirectories.begin();
				itr != allDirectories.end(); ++itr)
				delete itr->second;
		}
	};
	auto_ptr<directoryT> root;

	/* Helper function that splits a pathname into its separate components. */
	static pair<vector<string>, string> splitPath(const string& str);

	/* Helper functions to read from files. */
	static pair<string, directoryT*> parseDirectory(InputSource& input);
	static pair<string, vector<string> > parseFile(InputSource& input);

	/* Helper function to write to files. */
	static void outputDirectory(const pair<string, const directoryT*>, ofstream& output);
	static void outputFile(const pair<string, vector<string> >,  ofstream& output);

	/* Helper function to locate a directory in the tree structure. */
	directoryT* findDirectory(const vector<string>& path);
	const directoryT* findDirectory(const vector<string>& path) const;

	/* Given an input source, reads from that source. */
	void loadFromSource(InputSource& input);
	
	/* Needed so that we can see the reportError function. */
	using ErrorPolicy::reportError;
};

/* Helper macros that simplify the rest of the code.  GDF_TEMPLATE expands out into the proper
 * template declaration for GDFReader, while GDF_TEMPLATE_ARGS represents the arguments to
 * the template without the typename keyword.
 */
#define GDF_TEMPLATE template <typename ErrorPolicy, template <typename DerivedType> class ReaderPolicy>
#define GDF_TEMPLATE_ARGS ErrorPolicy, ReaderPolicy

/* Constructor sets the root directory to point to a new directory. */
GDF_TEMPLATE
GDFReader<GDF_TEMPLATE_ARGS>::GDFReader()
{
	clear();
}

/* Clear discards existing content and stores a new directory structure. */
GDF_TEMPLATE
void GDFReader<GDF_TEMPLATE_ARGS>::clear()
{
	root.reset(new directoryT);
}

/* Utility function that accepts a filename and returns a pair of the
 * path to the file and the name of the file.  For example, given
 * path/to/my/file, this would return the pair
 * < {path, to, my}, file >
 */
GDF_TEMPLATE
pair<vector<string>, string> GDFReader<GDF_TEMPLATE_ARGS>::splitPath(const string& str)
{
	pair<vector<string>, string> result;
	string::size_type curPos = 0;

	/* Keep on munching pieces off. */
	while(true)
	{
		/* Find the next separator character.  If we can't find one,
		 * stop looking.
		 */
		const string::size_type nextSlash = str.find('/', curPos);
		if(nextSlash == string::npos) break;

		/* Add this segment. */
		result.first.push_back(string(str.begin() + curPos, str.begin() + nextSlash));
		
		/* Update the current position. */
		curPos = nextSlash + 1;
	}
	result.second = str.substr(curPos);
	return result;
}

/* Given a reader, read from that source. */
GDF_TEMPLATE
void GDFReader<GDF_TEMPLATE_ARGS>::loadFromSource(InputSource& input)
{
	/* Open the input file, fail if we can't. */
	if(!input.isOpen())
	{
		reportError("Cannot open source!");
		return;
	}

	/* Skip over '[Directory]' */
	string line = input.readLine();

	/* Read in the directory structure and store it appropriately. */
	root.reset(parseDirectory(input).second);
}

/* Reads in a directory and hands back the appropriate information.  It first
 * scans the name of the directory, then reads in the contents by recursively
 * calling the appropriate parse function.
 */
GDF_TEMPLATE
pair<string, typename GDFReader<GDF_TEMPLATE_ARGS>::directoryT*>
GDFReader<GDF_TEMPLATE_ARGS>::parseDirectory(InputSource& input)
{
	/* First, read the name. */
	pair<string, directoryT*> result("", new directoryT);
	result.first = input.readLine();

	/* Now, march down reading things. */
	while(true)
	{
		string line = input.readLine();

		/* If we read the end-of-directory tag, just return what we have so far. */
		if(line == "[EndDirectory]") return result;

		/* If we get a file tag, read a file.  Conveniently, the return type of parseFile
		 * is a pair that we can directly insert into the file map, so this code is very
		 * short!
		 */
		if(line == "[File]")
			result.second->allFiles.insert(parseFile(input));

		/* If we get a directory read a directory.  As with above, the return type of
		 * readDirectory matches exactly what the map wants.
		 */
		if(line == "[Directory]")
			result.second->allDirectories.insert(parseDirectory(input));
	}
}

/* Helper function to read in a file.  This reads in the name of the file and the file body,
 * stopping at the end-of-file marker.
 */
GDF_TEMPLATE
pair<string, vector<string> > GDFReader<GDF_TEMPLATE_ARGS>::parseFile(InputSource& input)
{
	/* Read in the name of the file. */
	pair<string, vector<string> > result;
	result.first = input.readLine();

	/* Keep looping, reading in content, until we're done. */
	while(true)
	{
		string line = input.readLine();

		/* If we're at end of file, stop. */
		if(line == "[EndFile]")
			return result;
		
		/* Otherwise, read in the line, trim the space character, and store. */
		if(!line.empty() && line[0] == ' ')
			result.second.push_back(line.substr(1));
	}
}

/* To add a file, we figure out where the file goes, jump to that location, and
 * insert the contents.
 */
GDF_TEMPLATE
void GDFReader<GDF_TEMPLATE_ARGS>::addFile(const string& filename, const vector<string> &contents)
{
	/* Split the path into its component parts. */
	const pair<vector<string>, string> pathBreakdown = splitPath(filename);

	/* Search for the directory, failing if we can't find it. */
	directoryT* location = findDirectory(pathBreakdown.first);
	if(location == NULL)
	{
		reportError("Cannot find directory of file " + filename);
		return;
	}

	/* Insert the new file. */
	location->allFiles.insert(make_pair(filename, contents));
}

/* addDirectory jumps to the correct directory, then adds a new directory. */
GDF_TEMPLATE
void GDFReader<GDF_TEMPLATE_ARGS>::addDirectory(const string& filename)
{
	/* Split the path so it's easier to work with. */
	const pair<vector<string>, string> pathBreakdown = splitPath(filename);

	/* Find the directory where this goes, failing if not found. */
	directoryT* location = findDirectory(pathBreakdown.first);
	if(location == NULL)
	{
		reportError("Cannot find directory for new directory " + filename);
		return;
	}

	/* Insert the new directory. */
	location->allDirectories.insert(make_pair(filename, new directoryT));
}

/* Traces down to the final destination, then returns the contents of the file. */
GDF_TEMPLATE
vector<string> GDFReader<GDF_TEMPLATE_ARGS>::getFileContents(const string& str) const
{
	/* Split the path appropriately. */
	const pair<vector<string>, string> pathBreakdown = splitPath(str);

	/* Jump to the proper directory, failing if we can't. */
	const directoryT* location = findDirectory(pathBreakdown.first);
	if(location == NULL)
	{
		reportError("Cannot get contents of nonexistent file " + str);
		return vector<string>();
	}

	/* Find the file, failing if we can't */
	map<string, vector<string> >::const_iterator itr =
		location->allFiles.find(pathBreakdown.second);

	/* Return contents if able. */
	if(itr == location->allFiles.end())
	{
		reportError("Cannot find file " + str);
		return vector<string>();
	}
	return itr->second;
}

/* Helper function to descend into the proper directory.  If unable to do so, this function
 * returns NULL.
 */
GDF_TEMPLATE
const typename GDFReader<GDF_TEMPLATE_ARGS>::directoryT*
GDFReader<GDF_TEMPLATE_ARGS>::findDirectory(const vector<string>& path) const
{
	/* Start at the root. */
	const directoryT* curr = root.get();
	for(vector<string>::const_iterator itr = path.begin(); itr != path.end(); ++itr)
	{
		/* Find the next location, failing if it's not actually there. */
		const map<string, directoryT*>::const_iterator nextDir = curr->allDirectories.find(*itr);
		if(nextDir == curr->allDirectories.end())
			return NULL;

		/* Move there. */
		curr = nextDir->second;
	}
	return curr;
}

/* Non-const version of the function.  This works by calling the const version of the function to
 * get the result, then using const_cast to strip the const away from the result.  This pattern
 * comes up frequently in professional code, so it might be worth committing to memory.
 */
GDF_TEMPLATE
typename GDFReader<GDF_TEMPLATE_ARGS>::directoryT*
GDFReader<GDF_TEMPLATE_ARGS>::findDirectory(const vector<string>& path)
{
	return const_cast<directoryT*>(static_cast<const GDFReader*>(this)->findDirectory(path));
}

/* To write the file, we just recursively call outputDirectory on the root. */
GDF_TEMPLATE
void GDFReader<GDF_TEMPLATE_ARGS>::writeFile(const string& filename)
{
	/* Try opening the file, failing if we can't. */
	ofstream output(filename.c_str());
	if(!output.is_open())
	{
		reportError("Cannot write to file " + filename);
		return;
	}

	/* Print out the root! */
	outputDirectory(make_pair("root", root.get()), output);
}

/* Recursively outputs the contents of the directory to an output destination. */
GDF_TEMPLATE
void GDFReader<GDF_TEMPLATE_ARGS>::outputDirectory(const pair<string, const directoryT*> dir, ofstream& output)
{
	/* Output boilerplate. */
	output << "[Directory]" << endl;
	output << dir.first << endl;

	/* Output each subdirectory. */
	for(map<string, directoryT*>::const_iterator itr = dir.second->allDirectories.begin();
		itr != dir.second->allDirectories.end(); ++itr)
		outputDirectory(*itr, output);
	
	/* Output each file. */
	for(map<string, vector<string> >::const_iterator itr = dir.second->allFiles.begin();
		itr != dir.second->allFiles.end(); ++itr)
		outputFile(*itr, output);

	/* Output boilerplate. */
	output << "[EndDirectory]" << endl;
}

/* Prints out the contents of a file to the result. */
GDF_TEMPLATE
void GDFReader<GDF_TEMPLATE_ARGS>::outputFile(const pair<string, vector<string> > file,  ofstream& output)
{
	/* Output boilerplate. */
	output << "[File]" << endl;
	output << file.first << endl;

	/* Output file contents. */
	for(vector<string>::const_iterator itr = file.second.begin(); itr != file.second.end(); ++itr)
		output << ' ' << *itr << endl;

	/* Output end of file. */
	output << "[EndFile]" << endl;
}

/* Locates the proper file, then sets its contents. */
GDF_TEMPLATE
void GDFReader<GDF_TEMPLATE_ARGS>::setFileContents(const string& filename, const vector<string>& contents)
{
	/* Trace down to the file.  If we can't find it, abort. */
	const pair<vector<string>, string> pathBreakdown = splitPath(filename);
	directoryT* location = findDirectory(pathBreakdown.first);
	if(location == NULL)
	{
		reportError("Cannot set contents of nonexistent file " + filename);
		return;
	}

	/* Find the file.  If we can't, abort. */
	map<string, vector<string> >::iterator itr = location->allFiles.find(pathBreakdown.second);
	if(itr == location->allFiles.end())
	{
		reportError("Cannot set contents of nonexistent file " + filename);
		return;
	}
	
	/* Set contents. */
	itr->second = contents;
}

int main()
{
	/* A reader that writes to disk on error and reads from an ifstream. */
	GDFReader<WriteToDiskOnError, IOStreamReader> reader;

	reader.setOutputFile("out.txt");
	reader.readFile("myfile.gdf");
	
	vector<string> fileContents = reader.getFileContents("sub/anothersub/nested");
	copy(fileContents.begin(), fileContents.end(), ostream_iterator<string>(cout, "\n"));
	
	return 0;
}