// Robert Płóciennik G1ISI

#include <boost/program_options.hpp>

#include <iostream>
#include <fstream>
#include <iterator>
#include <vector>

namespace po = boost::program_options;

// vector<T> -> "T1 T2 T3..."

template<class T>
std::ostream& operator << ( std::ostream& os_, const std::vector<T>& vec_ )
{
    copy(vec_.begin(), vec_.end(), std::ostream_iterator<T>(std::cout, " ")); 
    return os_;
}

int main( int argc, char* argv[] )
{
	const char* defStr = "Sample string";
	const int defInt = 13;
	const char* envPrefix = "TEST_";
	
	try
	{	
		// options_description stanowi kontener przechowujący definiowane opcje
		// programu, które później można oczytywać parsując command-line, config
		// files, czy nawet dostępne zmienne środowiska w ramach kontenera.
		// Opcjonalnym argumentem konstruktora options_description jest caption
		// służący celom informacyjnym, gdy options_descriptor wypisywany jest
		// na wyjście (przeciążony operator <<).

		/**/ po::options_description argless( "Options without arguments" );

		// Dodawanie opcji do kontenera za pomocą metody add_options(), która
		// zwraca referencję na pomocniczy obiekt z przeciążonym
		// operatorem (), który z kolei zwraca referencję na samego siebie.
		// Pierwszym argumentem operatora () jest nazwa opcji, wraz z jej
		// opcjonalnym skrótem po przecinku, drugim - opis opcji.
		
		argless.add_options()
			( "help", "standard help" )
			( "long-help", "extended info" )
			( "version,v", "display version info and quit" )
		;

		// W powyższym przypadku, jeśli dane opcje będą wykorzystywane jako
		// command-line options, wówczas 'version' będzie rozpoznawana w linii
		// polecenia jako '--version' lub '-v'
		
		/**/ po::options_description withargs( "Options with arguments" );

		/**/ int int1var;
	
		// W celu zadeklarownia opcji przyjmujących argumenty, należy
		// w operatorze () umieścić jeszcze jeden argument świadczący o typie
		// danej opcji. Dany argument jest obiektem, uzyskiwanym przez wywołanie
		// konstruktora value< T >(), którego opcjonalnym argumentem może być
		// wskaźnik na zmienną, która przy odczytaniu danej opcji będzie
		// przechowywać jej wartość.
		// Można dodatkowo sprecyzować wartość domyślną danej opcji za
		// pośrednictwem metody default_value() nowo utworzonego obiektu.
		
		withargs.add_options()
			( "int1", po::value< int >(&int1var) )
			( "int2def", po::value< int >()->default_value(defInt), "Has default value" )
			( "str1", po::value< std::string >()->default_value(defStr), "With default value as well" )
			( "str2vector", po::value< std::vector<std::string> >(), "Vector of strings" )
		;
		
		// W powyższym przypadku, wartość opcji 'int1' po odczycie zachowana
		// będzie pod zmienną int1var
		
		/**/ po::options_description notToBeListed( "Positional options" );
		
		notToBeListed.add_options()
			( "configs", po::value< std::vector<std::string> >(), "Vector of strings (2 first params)" )
			( "ints", po::value< std::vector<int> >(), "Vector of ints (2 next params)" )
		;
		
		// positional_options_description zawiera opcje zadeklarowane w pewnym
		// options_description (aby możliwy był ich odczyt) i służy odczytywaniu
		// wartości opcji jako kolejnych argumentów podawanych w linii polecenia
		// bez precyzowania nazwy opcji jako argumentu
		
		/**/ po::positional_options_description pos;
		
		pos.add( "configs", 2 );
		pos.add( "ints", 2 );
		
		// W powyższym przypadku można oczekiwać, że podanie w linii polecenia:
		// $./exe abc def 123
		// byłoby jednoznaczne z:
		// $./exe --configs abc --configs def --ints 123
		
		// Zadeklarowanie opcji zczytywanych ze zmiennych środowiska
		
		/**/ po::options_description envVars( "Environment variables" );

		envVars.add_options()
			( "envint", po::value< int >(), "Int from environment" )
			( "envstr", po::value< std::string >(), "String from environment" )
		;

		// options_description można łączyć w bardziej ogólne options_description
		// celem wykonywania parsowania na całym zbiorze opcji, bądź ich
		// zgrupowanemu wyświetleniu.
		// (Metoda add() klasy zwraca referencję na jej obiekt).
		
		/**/ po::options_description helpInfo( "Available options" );
		helpInfo.add( argless ).add( withargs );
		
		/**/ po::options_description extInfo( "All the options" );
		extInfo.add( argless ).add( withargs ).add( notToBeListed ).add( envVars );
		
		/**/ po::options_description parseCmdLine;
		parseCmdLine.add( argless ).add( withargs ).add( notToBeListed );
		
		// Dzięki powyższemu zabiegowi:
		// * helpInfo zawiera te opcje, o których informacje będą wyświetlane
		//   przy uruchomieniu programu z parametrem/opcją --help
		// * extInfo zawiera informacje o opcjach wyświetlane dla parametru
		//   --long-help, w tym informacje o opcjach, które nie powinny być
		//   w ten sposób wyświetlane (notToBeListed, envVars)
		
		// variables_map zbliżona jest do mapy zawierająca przyporządkowanie
		// opcja -> wartość, bądź tylko wystąpienie danej opcji dla opcji
		// bezargumentowych, gdy w wyniku parsowania, opcje takie zostały
		// zczytane
		
		/**/ po::variables_map vm;
		
		try
		{
			// Wszystkie metody pomocniczej klasy command_line_parser zwracają
			// referencje do jej obiektu pozwalając na wykonanie następujących
			// operacji:
			// * parsowania linii poleceń pod kątem opcji dostępnych
			//   w parseCmdLine
			// * w tym uwzględniając positional options z 'pos'
			//
			// Efekt tej operacji ostatecznie zostaje przechowany w vm
			
			store( po::command_line_parser(argc,argv).options(parseCmdLine).positional(pos).run(), vm );
		}
		catch( std::exception& e_ )
		{
			std::cout << "While parsing command-line: " << e_.what() << "\n";
		}
		
		try
		{
			// Operacja parsowania zmiennych środowiskowych o prefiksie
			// envPrefix zawarych w envVars
			//
			// Wszelkie zmienne środowiska zgodne z prefiksem, lecz
			// nieuwzględnione w envVars bedą traktowane jako opcje nieznane
			// (tak samo, jak by to miało miejsce dla nieznanych paramterów
			// linii polecenia).
			// Dodatkowo nazwy opcji są przymusowo konwertowane do lowcase.
			//
			// Przykład:
			// 
			// Zmienna w środowisku: MY_ENV_VARIABLE="My data"
			// envPrefix = "MY_"
			// envVars = { "Env_Variable" }
			//
			// W efekcie w vm znajdzie się: vm["env_variable"] == "My data"
			
			store( po::parse_environment(envVars,envPrefix), vm );
		}
		catch( std::exception& e_ )
		{
			std::cout << "While parsing environment: " << e_.what() << "\n";
		}

		// Wymagana operacja odświerzenia stanu variables_map		
		po::notify( vm );

		// Sprawdzanie wystąpienia opcji, jest jednoznaczne ze sprawdzeniem jej
		// występowania w danej variables_map
		
		if( vm.count("configs") )
		{
			// Pliki sprecyzowane w opcji configs są kolejno parsowane.
			// Zapisane w nich przypisania opcja -> wartość nie nadpisują jednak
			// wartośći opcji zczytanych przy pomocy innych sposobów parsowania.
			//
			// Format pliku jest następujący:
			// 
			// <option> = <value>
			// #<commented_option>
			//
			// Przykładowo, wczytanie:
			//
			// str1 = A new string
			//
			// spowoduje nadpisanie domyślnej wartości dla opcji str1, choć ta
			// może zostać na powrot zmieniona chociążby przekazując wartość
			// opcji w linii polecenia
			
			/**/ std::ifstream file;
			/**/ unsigned i;
			/**/ const std::vector< std::string >* configsPtr = &(vm["configs"].as< std::vector<std::string> >());

			for( i = 0; i <= (unsigned) configsPtr->size(); ++i )
				try
				{
					file.open( (*configsPtr)[i].c_str() );					
					store( parse_config_file(file,withargs), vm );

					// W tym przypadku zczytywane są tylko wartości dla opcji
					// z 'withargs'					
				}
				catch( std::exception& e_ )
				{
					std::cout << "While parsing '" << (*configsPtr)[i] << "': " << e_.what() << "\n";
				}
		}

		// Wymagana operacja odświerzenia stanu variables_map		
		po::notify( vm );
		
		// Usage info
		if( vm.count("help") || vm.count("long-help") || argc == 1 )
		{
			std::cout << "./usage [OPTION <argument>] [configs{0}] [configs{1}] [ints{0}] [ints{1}]\n\n";
			
			// Zawartość options_description w postaci sformatowanej może zostać
			// wysłana na wyjście
			
			if( vm.count("long-help") )
				std::cout << extInfo << "\n";
			else
				std::cout << helpInfo << "\n";
		}
		
		if( vm.count("version") )
		{
			std::cout << "Uses libBoost::program_options 1.33.1\n";
			return 0;
		}
		
		// Wypisanie wartości danych opcji jeśli te zostały zczytane, bądź
		// posiadały wartośći domyślne

		std::cout << "Options set:\n\n";
		
		// Dostępu do wartośći opcji z variables_map należy dokonywać przy
		// użyciu metody as() ustalającej typ danej
		
		if( vm.count("int1") )
			std::cout << "  int1 == " << int1var << "\n";			
		if( vm.count("int2def") )
			std::cout << "  int2def == " << vm["int2def"].as< int >() << "\n";			
		if( vm.count("str1") )
			std::cout << "  str1 == " << vm["str1"].as< std::string >() << "\n";			
		if( vm.count("str2vector") )
			std::cout << "  str2vector == " << vm["str2vector"].as< std::vector<std::string> >() << "\n";			
		
		if( vm.count("configs") )
			std::cout << "  configs == " << vm["configs"].as< std::vector<std::string> >() << "\n";					
		if( vm.count("configs") )
			std::cout << "  ints == " << vm["ints"].as< std::vector<int> >() << "\n";					
		
		if( vm.count("envint") )
			std::cout << "  " << envPrefix << "ENVINT == " << vm["envint"].as< int >() << "\n";
		if( vm.count("envstr") )
			std::cout << "  " << envPrefix << "ENVSTR == " << vm["envstr"].as< std::string >() << "\n";
	}
	catch( std::exception& e_ )
	{
		std::cout << "While defining program options: " << e_.what() << "\n";
		return 1;
	}	
	
	return 0;
}
