#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;
}