#include <iostream>
#include <string>
#include <vector>
#include <getopt.h>
#include <libgen.h>
#include <cctype>
#include <algorithm>
#include <fstream>
#include <iterator>
#include <sstream>
#include <set>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <dirent.h>

using namespace std;

// Parse some options for the transform
static struct option long_options[] = {
	{"setup", no_argument, 0, 's'},
	{"install_dir", required_argument, 0, 'i'},
	{"manifest", required_argument, 0, 'm'},
	{"clean", no_argument, 0, 'c'},
	{"label", required_argument, 0, 'l'},
	{"verbose", no_argument, 0, 'v'},
	{"quiet", no_argument, 0, 'q'},
	{"help", no_argument, 0, 'h'},
	{"usage", no_argument, 0, '?'},
	{0,0,0,0}
};
static const auto short_opts="si:m:cl:vqh?";

void usage(int argc, char* argv[])
{
	cerr<<""<<argv[0]<<endl;
	cerr<<"		( --setup -m manifest.txt -i /path/to/installation -l label1 [-l label2 ...]  | "<<endl; 
	cerr<<"		-m manifest.txt  | "<<endl;
	cerr<<"		-m manifest.txt -i /path/to/installation -l label1 [-l label2 ...] )"<<endl;
	cerr<<"options are:"<<endl;
	cerr<<"		--setup,-s		Create manifest.txt.config files, but don't install."<<endl;
	cerr<<"		--clean,-c		Remove any .config files."<<endl;
	cerr<<"		--install_dir,-i	Specify a location to install files listed in the manifest."<<endl;
	cerr<<"		--manifest,-m		Specify manifest.txt.config files to use."<<endl;
	cerr<<"		--label,-l		Specify labels to install."<<endl;
	cerr<<"		--verbose,-v		Spew debug data."<<endl;
	cerr<<"		--quiet,-q		No output."<<endl;
	cerr<<"		--help,--usage,-h,-?	This information."<<endl;
}


typedef struct PEDI_options PEDI_options_t;
struct PEDI_options
{
	string manifest_files;
	string labels;
	string install_dir;
	bool setup_only;
	bool clean;
	bool verbose;
	bool quiet;

	PEDI_options() : setup_only(false), clean(false), verbose(false) { }
};


auto my_options=PEDI_options_t();

void parse_args(int argc, char* argv[])
{

        while(1) 
	{
                auto index = 0;
                auto c = getopt_long(argc, argv,short_opts, long_options, &index);
                if(c == -1)
                        break;
                switch(c) 
		{
                        case 0:
                                break;
			case 's':
				my_options.setup_only=true;
				break;
			case 'c':
				my_options.clean=true;
				break;
			case 'i':
				if(my_options.install_dir!="")
				{
					cerr<<"Install dir cannot be specified multiple times"<<endl;
					exit(1);
				}
				my_options.install_dir=string(optarg);
				break;
			case 'm':
				my_options.manifest_files+=string(" ")+optarg;
				break;
			case 'l':
				my_options.labels+=string(" ")+optarg;
				break;
			case 'v':
				my_options.verbose=true;
				break;
			case 'q':
				my_options.quiet=true;
				break;
			case 'h':
			case '?':
				usage(argc,argv);
				exit(1);
				break;
		}
	}

	if(my_options.manifest_files=="")
	{
		cerr<<"Must specify a manifest file."<<endl;
		exit(1);
	}

	if(my_options.setup_only)
	{
		if(my_options.install_dir=="")
		{
			cerr<<"Must specify an install path with --setup."<<endl;
			exit(1);
		}
		if(my_options.labels=="")
		{
			cerr<<"Must specify labels to install with--setup."<<endl;
			exit(1);
		}
	}
	else
	{
		if(my_options.install_dir=="" && my_options.labels!="")
		{
			cerr<<"Cannot set install dir without setting labels."<<endl;
			exit(1);
		}
		if(my_options.install_dir!="" && my_options.labels=="")
		{
			cerr<<"Cannot set labels without setting install dir."<<endl;
			exit(1);
		}
	}
}



// trim from start (in place)
static inline void ltrim(std::string &s) {
    s.erase(s.begin(), std::find_if(s.begin(), s.end(),
            std::not1(std::ptr_fun<int, int>(std::isspace))));
}

// trim from end (in place)
static inline void rtrim(std::string &s) {
    s.erase(std::find_if(s.rbegin(), s.rend(),
            std::not1(std::ptr_fun<int, int>(std::isspace))).base(), s.end());
}

// trim from both ends (in place)
static inline void trim(std::string &s) {
    ltrim(s);
    rtrim(s);
}

// trim from start (copying)
static inline std::string ltrimmed(std::string s) {
    ltrim(s);
    return s;
}

// trim from end (copying)
static inline std::string rtrimmed(std::string s) {
    rtrim(s);
    return s;
}

// trim from both ends (copying)
static inline std::string trimmed(std::string s) {
    trim(s);
    return s;
}

vector<string> split(const char *str, char c = ' ')
{
    auto result=vector<string>();

    do
    {
        const auto begin = str;

        while(*str != c && *str)
            str++;

	auto token=string(begin, str);
	if(trimmed(token)!="")
       		result.push_back(token);
    } while (0 != *str++);

    return result;
}

vector<string> manifest_files;
set<pair<string,string>> install_files;

void include_file(const string &file, const string &label)
{
	auto rp=realpath(trimmed(file).c_str(),NULL);
	if(rp==NULL)
	{
		cerr<<"Cannot find path to '"<<file<<"'"<<endl;
		perror("include_file");
		exit(1);
	}
	auto rp_str=string(rp);
	// free the realpath allocation
	free(rp);	// realpath with NULL 


	install_files.insert(pair<string,string>(rp_str,trimmed(label)));

	if(my_options.verbose)
	{
		cout<<"found file ='"<<rp_str<<"', label='"<<trimmed(label)<<"'"<<endl;
	}
}

void include_dir(const int line_no, const string base_dir, const string sub_dir, const string label,  bool recursive=false)
{

	DIR *dir=NULL;
	struct dirent *ent=NULL;
	const auto dir_to_open=base_dir+"/"+sub_dir;
	if ((dir = opendir(dir_to_open.c_str())) != NULL) 
	{
		/* print all the files and directories within directory */
		while ((ent = readdir (dir)) != NULL) 
		{
			// skip . and ..
			if(string(".")==ent->d_name || string("..")==ent->d_name)
				continue;

			const auto full_filename=dir_to_open+"/"+ent->d_name;
			const auto full_subdir_name= sub_dir+"/"+ent->d_name;
			if(ent->d_type==DT_REG)
			{
				include_file(full_filename, label);
			}
			else if(ent->d_type==DT_DIR)
			{
				if(recursive) 
					include_dir(line_no, base_dir, full_subdir_name, label, recursive);
			}
			else if(ent->d_type==DT_UNKNOWN)
			{
				/* ok, resort to actually stat-ing the file */
				struct stat fileinfo={0};
				if(stat(full_filename.c_str(), &fileinfo)!=0)
				{
					cerr<<"Cannot open file: "<<full_filename<<endl;
					perror("");
					exit(1);
				}
				if(S_ISREG(fileinfo.st_mode))
					include_file(full_filename, label);
				else if(S_ISDIR(fileinfo.st_mode))
				{
					if(recursive)	
						include_dir(line_no, base_dir, full_subdir_name, label, recursive);
				}
	
			}

		}
		closedir (dir);
	} 
	else 
	{
		/* could not open directory */
		cerr<<"At line "<<line_no<<": "<<" Cannot open "<<dir_to_open<<endl;
		perror ("");
		exit(1);
	}
}


void find_manifest_file(string &file)
{
	/* caution!  this useage of realpath allocates memory */
	auto rp=realpath(trimmed(file).c_str(),NULL);
	if(rp==NULL)
	{
		cerr<<"Cannot find path to "<<file<<endl;
		exit(1);
	}
	auto rp_str=string(rp);

	/* caution!  dirname modifies it's argument. */
	auto dn=dirname(rp);
	auto dn_str=string(dn);

	// free the realpath allocation
	free(rp);	// realpath with NULL 


	file=rp_str; // update manifest_files vector with full path to manifest file


	// parse the manifest briefly, looking for submanifests.
	// note:  can't use auto input= format due to no copy constructor for this class.
	ifstream input(rp_str);
	if(!input.is_open())
	{
		cerr<<"Cannot open manifest file: "<<rp_str<<endl;
		exit(1);
	}
	auto line_no=0;
	for( auto line=string() ; getline(input,line); )
	{
		auto tokens=split(trimmed(line).c_str(),' ');
		if(tokens.size() == 0)
		{
			/* ignore */
		} 
		else if(tokens[0]=="submanifest")
		{
			if(tokens.size() != 2)
			{
				cerr<<"Too many/few tokens for 'manifest' directive on line "<<line_no<<endl;
				cerr<<"line='"<<line<<"'"<<endl;
				exit(1);
			}
			manifest_files.push_back(dn_str+"/"+tokens[1]);
		}
		else if(tokens[0]=="file")
		{
			if(tokens.size() != 3)
			{
				cerr<<"Too many/few tokens for 'file' directive on line "<<line_no<<endl;
				cerr<<"line='"<<line<<"'"<<endl;
				exit(1);
			}
			if(my_options.clean || my_options.setup_only)
			{ 
				// do nothing 
			}
			else
			{
				include_file(dn_str+"/"+tokens[1], tokens[2]);
			}
		}
		else if(tokens[0]=="recursive_directory")
		{
			if(my_options.clean || my_options.setup_only)
			{ 
				// do nothing 
			}
			else
			{
				if(tokens.size() != 3)
				{
					cerr<<"Too many/few tokens for 'recursive_directory' directive on line "<<line_no<<endl;
					cerr<<"line='"<<line<<"'"<<endl;
					exit(1);
				}
				include_dir(line_no, dn_str, tokens[1],tokens[2], true);
			}
		}
		else if(tokens[0]=="directory")
		{
			if(my_options.clean || my_options.setup_only)
			{ 
				// do nothing 
			}
			else
			{
				if(tokens.size() != 3)
				{
					cerr<<"Too many/few tokens for 'directory' directive on line "<<line_no<<endl;
					cerr<<"line='"<<line<<"'"<<endl;
					exit(1);
				}
				include_dir(line_no, dn_str, tokens[1], tokens[2], false);
			}
		}
		else if(tokens[0]=="" || tokens[0][0]=='#')
		{
			// ignore
		}
		else
		{
			cerr<<"At line "<<line_no<<": Syntax error at token '"<<tokens[0]<<"'."<<endl;
			exit(1);
		}
		line_no++;
	}
	
}


string manifest_prefix;
void calc_top_manifest_prefix()
{
	auto dn=strdup(manifest_files[0].c_str());
	auto prefix_str=dirname(dn);
	manifest_prefix=string(prefix_str);
	free(dn);
}

void setup()
{
	// split up the input string and push each one into the vector of all manifest files.
	// note: cannot use auto style initilizer because of deleted copy constructor.
        //auto mf=istringstream(my_options.manifest_files);
        istringstream mf(my_options.manifest_files);
	manifest_files.insert(end(manifest_files), istream_iterator<string>(mf), istream_iterator<string>());

	find_manifest_file(manifest_files[0]);
	calc_top_manifest_prefix();

	for(int i=1;i<manifest_files.size();i++)
	{
		find_manifest_file(manifest_files[i]);
	}
	

}

void clean()
{
	for(const auto & s : manifest_files)
	{
		auto configfile=trimmed(s)+".config";

        	struct stat sb;
		auto stat_res=stat(configfile.c_str(), &sb);

		if(stat_res==0)
		{
			
			if(my_options.verbose)
				cout<<"Unlink "<<configfile<<endl;
			auto res=unlink(configfile.c_str());
			if(res!=0)
			{
				cerr<<"Cannot unlink file: "<<configfile<<endl;
				perror("");
				exit(1);
			}
		}
	}
}

void update_configs()
{
	// only write the configs if updated labels are used.
	if(my_options.labels=="")
		return;

	for(const auto & s : manifest_files)
	{
		auto configfile=trimmed(s)+".config";
		ofstream out(configfile, ofstream::out | ofstream::trunc);
		if(!out.is_open())
		{
			cerr<<"Cannot open config file: "<<configfile<<endl;
			exit(1);
		}
		auto install_dir=s;
		install_dir.erase(0,manifest_prefix.size());
		install_dir=my_options.install_dir+string("/")+install_dir;
		auto dn=strdup(install_dir.c_str());
		auto dn_dir=dirname(dn);
		auto dn_dir_str=string(dn_dir);
		free(dn);
		out<<dn_dir_str<<endl;
		out<<my_options.labels<<endl;
	}
}


void read_config(const string &configfile, string &install_dir, string &labels)
{
	ifstream in(configfile);
	if(!in.is_open())
	{
		cerr<<"Cannot open config file: "<<configfile<<endl;
		exit(1);
	}
	if(!getline(in,install_dir))
	{
		cerr<<"Cannot read "<<configfile<<endl;
		exit(1);
	}
	if(!getline(in,labels))
	{
		cerr<<"Cannot read "<<configfile<<endl;
		exit(1);
	}
}

auto configs_read=false;

void read_configs()
{
	// only read the configs if no labels are specified.
	if(my_options.labels!="")
		return;
	configs_read=true;
	const auto mf=manifest_files[0];
	auto orig_configfile=trimmed(mf)+".config";

	read_config(orig_configfile, my_options.install_dir, my_options.labels);

#if 0
	for(const auto & s : manifest_files)
	{
		string install_dir, labels;
		auto configfile=trimmed(s)+".config";
		read_config(configfile, install_dir, labels);

		if(my_options.install_dir != install_dir || my_options.labels != labels)
		{
			cerr<<"Mismatch configuration in "<<orig_configfile<<" and "<<configfile<<endl;
			exit(1);
		}
	}
#endif
}

auto previously_had_pedi_root=false;

void remove_stale()
{
        struct stat sb;
	auto pedi_root_fn=my_options.install_dir+"/.pedi_root";
	auto stat_res=stat(pedi_root_fn.c_str(), &sb);
	if(stat_res==0)
	{
		if(my_options.verbose)
			cout<<"Detected install dir as pedi_root install location."<<endl;
		previously_had_pedi_root=true;
	}

	auto cmd=string("/bin/rm -Rf /"+my_options.install_dir);
	auto res=system(cmd.c_str());
	if(res!=0)
	{
		cerr<<"Cannot remove install directory: "<<my_options.install_dir<<endl;
		exit(1);
	}
}

void create_install_dir(const string &file)
{
	auto tokens=split(file.c_str(), '/');

	string full_path="";
	tokens.pop_back();
	for(const auto &s :  tokens)
	{
		full_path+=string("/")+s;
		mkdir(full_path.c_str(), S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IWGRP | S_IXGRP | S_IROTH | S_IWOTH | S_IXOTH);
	}
}

void install()
{
	const auto mf=manifest_files[0];
	


	if(install_files.size()==0)
	{
		cerr<<"PEDI: No files to install! (did you remember --label)?"<<endl;
		exit(0);
	}
	
	auto installed_files=0;
	for(const auto & ifp : install_files)
	{
		const auto &file=ifp.first;
		const auto &label=ifp.second;
	
		istringstream labels(my_options.labels);
		auto found=find_if(istream_iterator<string>(labels),
			istream_iterator<string>(), [&](const string & word)
			{ return trimmed(word)==trimmed(label); });

		if(found==istream_iterator<string>())
		{
			if(my_options.verbose)
			{
				cout<<"Eliding file='"<<file<<"', label='"<<label<<"'"<<endl;
			}
		}
		else
		{
			string outfile=file;
			outfile.erase(0,manifest_prefix.size());
			outfile=my_options.install_dir+"/"+outfile;
			if(my_options.verbose)
				cout<<"Installing file "<<file<<" to " <<outfile<<endl;
			installed_files++;

			struct stat stat_buf;
			auto res=stat(file.c_str(),&stat_buf);
			if(res!=0)
			{
				cerr<<"Error acessing file: "<<file<<endl;
				perror("");
				exit(1);
			}

			create_install_dir(outfile);
			ifstream  src(file,  ios::binary);
			ofstream  dst(outfile,   ios::binary);
			dst << src.rdbuf();
			dst.close();
			auto res2=chmod(outfile.c_str(), stat_buf.st_mode);
			if(res2!=0)
			{
				cerr<<"Error acessing file: "<<outfile<<endl;
				perror("");
				exit(1);
			}
		}
	}

	if(!my_options.quiet)
		cout<<"PEDI: Installed "<<installed_files<<" files."<<endl;
}

void finish()
{
	if(!configs_read || previously_had_pedi_root)
	{
		ofstream o(my_options.install_dir+"/"+".pedi_root");
		if(my_options.verbose)
			cout<<"Writing pedi_root file."<<endl;
		return;
	}
	if(my_options.verbose)
		cout<<"Skipping pedi_root as partial install"<<endl;
}

int main(int argc, char* argv[])
{
	parse_args(argc,argv);
	if(!my_options.quiet)
		cout<<"Welcome to the Program Executable and Dependency Installer (PEDI) tool"<<endl;

	setup();
	if(my_options.clean)
	{
		clean();
		if(!my_options.quiet)
			cout<<"PEDI: Cleanup complete!"<<endl;
		return 0;
	}
	update_configs();
	read_configs();

	// finished after setup.
	if(my_options.setup_only)
	{
		finish();
		return 0;
	}

	if(my_options.verbose)
	{
		cout<<"Installing files that match labels='"<<my_options.labels<<"' to "<<my_options.install_dir<<endl;
	}
	remove_stale();
	install();
	finish();
	if(!my_options.quiet)
		cout<<"PEDI: Install complete!"<<endl;

	return 0;
}