#include <iostream>
#include <fstream>
#include <sstream>
#include <vector>
#include <list>
#include <map>
#include <string>
#include <assert.h>
#include "sha1.hpp"
#include "alexshatorrent05.cpp"
#define verbosedecode 0
#define print_error_bad_torrent 1
#define debugprint 0
#define how_many_bytes 10

using namespace std;

string path(string n){
	char backslash=0x5c;
	std::size_t slashpos = n.rfind(backslash);
	if (slashpos==string::npos)		return "";
	else 							return n.substr(0,slashpos);}	//does not include the slash at end
string filename(string n){
	char backslash=0x5c;
	std::size_t slashpos = n.rfind(backslash);
	if (slashpos==string::npos)		return n;
	else 							return n.substr(slashpos+1);}	// does not include slashes
void printvector(vector<string> &v){
	for (int i=0;i<v.size();i++)
		cout<<v[i]<<endl;
}
void printvectorskipslash(vector<string> &v){
	for (int i=0;i<v.size();i++)
		cout<<v[i].substr(1)<<endl;
}
bool is_digit(char n){
	return ('0'<=n and n<='9');}
string indent(int n){
	return string(4*n,' ');
}
string hex(string n){
	string return_string;
	char hex[]="0123456789abcdef";
	for (int i=0;i<how_many_bytes;i++){
		return_string.append(1,hex[(n[i] & 0xf0)>>4]);
		return_string.append(1,hex[(n[i] & 15)]);
	}
	return return_string;
}
int string_to_int(string n){
	istringstream instr(n);
	int output;
	instr >> output;
	return output;
}
// There are two functions that are similarily named.
// Because the same functionality is needed to decode strings
// for map keys I have two functions this decodes the string and outputs in in r
// and the other takes that r and saves it in the btitem
int string_decode_string(ifstream &n,int depth, string &r){
	string str;
	char ch;
	n.get(ch);
	if(!(is_digit(ch)))		return 1;
	while(is_digit(ch)){
		str.append(1,ch);
		n.get(ch);
	}
	if(!(ch==':'))			return 1;
	int size(string_to_int(str));
	for (int i=0;i<size;i++){
		n.get(ch);
		r.append(1,ch);
	}
	if (verbosedecode){
		if(r.size()<200)	cout<<indent(depth)<<r<<endl;
		else 				cout<<indent(depth)<<hex(r)<<"... hashstring size()="<<r.size()<<"\n";}
	return 0;
}
long long int string_to_long_int(string n){
	istringstream instr(n);
	long long int output;
	instr >> output;
	return output;
}
class btitem {
	public:
	btitem(){
		itemtype=0;
		depth=1;
		i=0;
	}
	btitem(const btitem &n){
		itemtype=n.itemtype;
		depth=n.depth;
		s=n.s;
		i=n.i;
		l=n.l;
		m=n.m;
	}
	
int decode_string(ifstream &n){
	itemtype=1;
	if (string_decode_string(n, depth, s)) return 1;
	return 0;
}
	
int decode_integer(ifstream &n){
	itemtype=2;
	string str;
	char ch;
	n.get(ch);
	if(!(ch=='i'))			return 1;
	n.get(ch);
	while ( '0' <= ch and ch <= '9' or ch=='-'){
		str.append(1,ch);
		n.get(ch);
	}
	if(!(ch=='e'))			return 1;
	i = string_to_long_int(str);
	if (verbosedecode)	cout<<indent(depth)<<i<<endl;
	return 0;
}
	
int decode_list(ifstream &n){
	itemtype=3;
	if (verbosedecode)	cout<<indent(depth)<<"list{\n";
	char ch;
	n.get(ch);
	if(!(ch=='l'))							return 1;
	while (n.peek()!='e'){
		btitem tempbtitem;
		tempbtitem.depth=depth+1;
		if (tempbtitem.decode_item(n))		return 1;
		l.push_back(tempbtitem);
	}
	n.get(ch);
	if(!(ch=='e'))							return 1;
	if (verbosedecode)	cout<<indent(depth)<<"}\n";
	return 0;
}
			
int decode_dictionary(ifstream &n){
	itemtype=4;
	if (verbosedecode)	cout<<indent(depth)<<"map{\n";
	char ch;
	n.get(ch);
	if(!(ch=='d'))			return 1;
	while (is_digit(n.peek())){		// could do not equal e for closing
		string tempstring;
		if (string_decode_string(n,depth+1,tempstring))	return 1;
		btitem tempbtitem;
		tempbtitem.depth=depth+1;
		if (tempbtitem.decode_item(n))					return 1;
			
		m[tempstring]=tempbtitem;
		if (verbosedecode)	cout<<endl;
	}
	n.get(ch);
	if(!(ch=='e'))			return 1;
	if (verbosedecode)	cout<<indent(depth)<<"}\n";
	return 0;
}
	
int decode_item(ifstream &input){
	switch (input.peek()){
		case 'd':
			return decode_dictionary(input);
			break;
		case 'i':
			return decode_integer(input);
			break;
		case 'l':
			return decode_list(input);
			break;
		case '0':
		case '1':
		case '2':
		case '3':
		case '4':
		case '5':
		case '6':
		case '7':
		case '8':
		case '9':
			return decode_string(input);
			break;
		default:
			return 1;
	}
}

int decode_item_from_file(const string &n){
	ifstream in(n, std::ios::binary);
	if(!in.is_open()) return 2;
	if (decode_item(in)) return 1;
	in.close();
	return 0;
}

void print(){
	switch (itemtype){
		case 1:
			if (s.size()<200)	cout<<indent(depth)<<s<<endl;
			else 	cout<<indent(depth)<<hex(s)<<"... #pieces= "<<s.size()/20<<"\n"<<endl;
			break;
		case 2:
			cout<<indent(depth)<<i<<endl;
			break;
		case 3:
			cout<<indent(depth)<<"list{\n";
			for (int i=0;i<l.size();i++)
				(l[i]).print();
			cout<<indent(depth)<<"}\n";
			break;
		case 4:
			cout<<indent(depth)<<"map{\n";
			for (std::map<string,btitem>::iterator mit=m.begin();mit!=m.end();mit++){
				cout<<indent(depth+1)<<mit->first<<endl;
				(mit->second).print();
				cout<<endl;
			}
			cout<<indent(depth)<<"}\n";
			break;
		case 0:
			assert(0);
			cout<<"Empty btitem\n";
		default :
			assert(0);
	}
}
	
vector<string> get_filenamelist(string dir){
	char slash[] = {0x5c, 0x00};
	vector<string> filenamelist;
	if (!m["info"].m.count("files")){
		filenamelist.push_back(dir+slash+m["info"].m["name"].s);
		return filenamelist;
	}
	for (int fn_i=0;fn_i<m["info"].m["files"].l.size();fn_i++){
		string r;
		r.append(dir);
		r.append(slash);
		vector<btitem> &paths = m["info"].m["files"].l[fn_i].m["path"].l;
		
		int size=paths.size();
		for (int i=0;i<size-1;i++){
			r.append(paths[i].s);
			r.append(slash);		
		}
		r.append(paths[size-1].s);
		filenamelist.push_back(r);
	}
	return filenamelist;
}

int get_piecelength(){
	return m["info"].m["piece length"].i;
}
int get_numberofpieces(){
	return m["info"].m["pieces"].s.size()/20;
}
vector<uint32_t> get_hashints(int index){
	vector<uint32_t> r;
	string &hash = m["info"].m["pieces"].s;
//	uint32_t a[5];
	int i=index*20;
	for (int j=0;j<5;j++)
    {
        r.push_back( (hash[j*4+i+3] & 0xff)
                   | (hash[j*4+i+2] & 0xff)<<8
                   | (hash[j*4+i+1] & 0xff)<<16
                   | (hash[j*4+i+0] & 0xff)<<24	);
//        cout<<r[j]<<endl;
    }
//    cout<<endl;
	return r;
}


	int 				itemtype;		// 1 string 2 integer 3 list 4 dictionary
	int					depth;
	string				s;
	long long int		i;
	vector<btitem>		l;
	map<string,btitem>	m;
		
		
};
	#define printtorrent 1
	#define debug 1
	#define vbv 1
class torrent{
	public:

	torrent(string &tor, string &dir){
		torrent_filename=tor;
		contents_directory=dir;
	}
	torrent(int c, const char*& a, const char*&b){
		if (debug) cout<<c<<"openconstructor\n";
		if (c>=2) torrent_filename=a; else return;
		if (debug) cout<<"fn assigned\n";
		if (c>=3) contents_directory=b;
		else contents_directory=path(torrent_filename);
		if (debug) cout<<"dir assigned\n";
		if (debug) cout<<torrent_filename<<endl<<contents_directory<<endl<<"endconstructor";
	}
	long long int get_combined_size(){
		//if(d.m.count["info"] and d.m["info"].m["files"])
		long long int r=0;
		if (debug) cout<<d.m["info"].m["files"].l.size()<<endl;
		for (int i=0;i<d.m["info"].m["files"].l.size();i++)
			r+= d.m["info"].m["files"].l[i].m["length"].i;
		return r;
	}
	string checksum_number(int index){
		char hex[]="0123456789abcdef";
		string return_string;
		for (int i=index*20;i<index*20+20;i++){
			return_string.append(1,hex[(d.m["info"].m["pieces"].s[i] & 0xf0)>>4]);
			return_string.append(1,hex[(d.m["info"].m["pieces"].s[i] & 15)]);	
		}
		return return_string;
	}
	
	int hashpiece(std::ifstream &is, int hashindex){
		SHA1 sha;
		int sentinel=sha.update_hashpiece(is, piecelength, fileindex, filenamelist);
		if (sentinel) return sentinel;
		if (sha.final()==checksum_number(hashindex))
			return 0;
		return 1;
	}
	string loadpiecefromstream(std::ifstream &is){
		string r,small;
		#define read_size_exponent 15
		int read_size=1<<read_size_exponent;
		#define read_size 64
//		#define fbuffer_size 64
//		#define BLOCK_BYTES 64
//		int gcount=0;
		#define printswitchfile 0
		int prevgcount=0;
		while (r.size()<piecelength and fileindex<filenamelist.size()){
			char buffer[read_size];
			is.read(buffer,read_size);
			r.append(buffer,is.gcount());
			prevgcount=is.gcount();
			while(prevgcount<read_size){
				is.close();				
				if (++fileindex==filenamelist.size()) break;
				is.open(filenamelist[fileindex], std::ios::binary);
				if(!is.is_open()) return "";
				if (printswitchfile) cout<<"switching to next file\n"<<filenamelist[fileindex]<<endl;
				prevgcount=is.gcount();
				is.read(buffer, read_size - is.gcount());
				r.append(buffer, is.gcount());
				prevgcount+=is.gcount();
			}

		}
	return r;
}
	int hashpiecestring(std::ifstream &is, int hashindex){
		string piece=loadpiecefromstream(is);
		assert(piece.size()==piecelength or fileindex==filenamelist.size());
		SHA1 sha;
		sha.update(piece);
		if (sha.final()==checksum_number(hashindex))
			return 0;
		return 1;
	}
	int load_stream_and_filelist(std::ifstream &stream){
		// contents in contents_directory
		// works for single file torrents because I coded that case in get_filenamelist()
		filenamelist = d.get_filenamelist(contents_directory);
		stream.open(filenamelist[0], std::ios::binary);
		if (stream.is_open()) return 0;
				
		// next try a subdirectory with the torrent name
		filenamelist.clear();
		if (d.m.count("info") and d.m["info"].m.count("name") ) 
			contents_directory = path(torrent_filename)+"\\"+d.m["info"].m["name"].s;
		filenamelist = d.get_filenamelist(contents_directory);
		stream.open(filenamelist[0], std::ios::binary);
		if (stream.is_open()) return 0;
		
		//	backup routine to directly input single file torrent
		filenamelist.clear();
		filenamelist.push_back(contents_directory);
		if (debug){printvector(filenamelist);
			cout<<"filenamelistsize="<<filenamelist.size()<<endl<<"contents_directory= "<<contents_directory<<endl;}
		stream.open(contents_directory, std::ios::binary);
		if (stream.is_open()) return 0;
			
		return 1;			
	}
	int verify_torrent(){
		if (debug){
			cout<<"torrent_filename="<<torrent_filename<<endl<<"contents_directory="<<contents_directory<<endl;
		}
		int r;
		r = d.decode_item_from_file(torrent_filename);
		if (r) {cout<<"d.decode_item_from_file() failed\nThat means source is invalid bencoded file\n"; return r;}
		else cout<<"Torrent sucessfully decoded.\n";
		if (printtorrent) d.print();
		
		ifstream stream;
		r=load_stream_and_filelist(stream);
		if (debug) cout<<"load_stream_and_filelist(stream) returned:"<<r<<"\nfilenamelist.size()="<<filenamelist.size()<<endl;
		if (r) return r;
		if (vbv) {
			vector<string> nopath=d.get_filenamelist("");
			cout<<"begin filenamelist\n";
			printvectorskipslash(nopath);
//			printvector(filenamelist);
			cout<<"end filenamelist\n\n";}
		if (vbv) cout<<"combined size="<<get_combined_size();
		piecelength = d.get_piecelength();
		if (vbv) cout<<" piecelength= "<<piecelength<<endl;
		numberofpieces = d.get_numberofpieces();
		if (vbv) {cout<<"numberofpieces= "<<numberofpieces<<endl<<"filenamelist[0]="<<filenamelist[0]<<"\nhashing";}
		time_t starttime=time(0);
		fileindex=0;
		int counter=0;
		for (int pieceindex=0;pieceindex<numberofpieces;pieceindex++){
			if (debug) cout<<".";
			if ( hashpiecestring(stream, pieceindex) == 0 ) counter++;
			else break;
		}
		cout<<"counter= "<<counter<<endl;
		if (counter==numberofpieces){
			time_t stoptime=time(0);
//			double elapsed=difftime(stoptime,starttime);
			double elapsed=stoptime-starttime;
			cout<<"\ntorrent verified\n";
			cout<<setprecision(3)<<"difftime(stoptime,starttime)="<<elapsed<<"\n"<<numberofpieces*piecelength/1000000/elapsed<<" MB/s\n";}
		else cout<<"\ntorrent verification failed";
	}
		string torrent_filename;
		string contents_directory;
		btitem d;
		int piecelength;
		int numberofpieces;
		int fileindex;		
		vector<string> filenamelist;
//		std::ifstream is;
};

void printhelp(){
	cout<<"\nAlex's Torrenthc\n";
	cout<<"first argument: torrentfile\n";
	cout<<"second argument: directory containing files or filename of single file \n";}

int main(int argc, const char *argv[]){
	cout<<"argc="<<argc<<endl;
	if (argc<2) {printhelp(); return 0;}
	torrent t(argc, argv[1], argv[2]);
	cout<<"constructor finished\n";
	t.verify_torrent();
	char c;
	if (debug) cout<<"Program ran correctly. Press enter to exit.\n";
	cin.get(c);
	return 0;
}