#include "ranking.h"
#include <fstream>
#include <cmath>
#include <ctime>
#include <cstdlib>
using std::ofstream;
using std::ifstream;

bool validDescription(string descript)
{
   // just needs to contain at least one non-whitespace character to be considered valid
   for (unsigned int i = 0; i < descript.length(); i++) {
       if (!isspace(descript[i])) {
          return true;
       }
   }
   return false;
}

bool readRankings(string filename, ItemList &rankings)
{
   // try to open the file for writing
   ifstream infile;
   infile.open(filename);
   if (!infile.is_open()) {
      cerr << "Error: unable to open file " << filename << " for reading" << endl;
      return 0;
   }

   // read the rankings until end-of-file, keep track of how many valid items were read
   int numItems = 0;
   while (!infile.eof()) {
      int rk;
      string desc, junkline;
      // the first thing should always be an integer rank
      infile >> rk;
      if (infile.eof()) {
         break; // we've processed the last available item
      } else if (infile.fail()) {
         // the current line didn't begin with an int, throw away the line
         infile.clear(); // fix the error state
         getline(infile, junkline);
         cerr << "Error: invalid rank, discarding line" << endl;
         cerr << "   " << junkline << endl;
      } else {
         // the rest of the line is the description
         getline(infile, desc);
         // make sure the description exists and is valid (not just whitespace)
         if (infile.eof() || !validDescription(desc)) {
            cerr << "Error: invalid description, discarding line" << endl;
            cerr << "   " << rk << desc << endl;
         } else {
            // create and fill in the new item
            ItemRank* current = new ItemRank;
            current->rank = rk;
            current->description = desc;
            current->next = nullptr;
            // insert the new item at the back of the list
            if (!insertBack(rankings, current)) {
               cerr << "Error: insert of new item into rankings failed" << endl;
            } else {
               numItems++;
            }
         }
      }
   }

   // close the file
   infile.close();
   rankings.size = numItems;
   return numItems;
}

bool writeRankings(string filename, ItemList rankings)
{
   // try to open the file for writing
   ofstream outfile;
   outfile.open(filename);
   if (!outfile.is_open()) {
      cerr << "Error: unable to open file " << filename << " for writing" << endl;
      return false;
   }

   // write each ranking, in sequence
   ItemRank *current = rankings.front;
   while (current != nullptr) {
       outfile << current->rank << " ";
       outfile << current->description << endl;
       current = current->next;
   }

   // close the file
   outfile.close();
   cout << "\nUpdated rankings written back to file " << filename << endl;
   return true;
}

void deallocate(ItemList &rankings)
{
   // keep removing/deleting items until the list is empty
   while (rankings.front) {
     ItemRank* curr = removeFront(rankings);
     delete curr;
   }
   initList(rankings);  // paranoid step, explicitly re-initializing as empty list
}

bool runMatchup(ItemList &rankings)
{
   // keep track of whether or not the random number generator has been seeded
   // (will be useful if we run more than one matchup)
   static bool Seeded = false;
   if (!Seeded) {
      srand(time(NULL));
      Seeded = true;
   }

   // keep track of positions of the matchup items in rankings
   int it1pos, it2pos;

   if (rankings.size < 2) {
      cerr << "Error: unable to run a head-to-head matchup ";
      cerr << "as there are fewer than two items" << endl;
      return false;
   } else if (rankings.size == 2) {
      // there is only one possible matchup, assign it
      it1pos = 0;
      it2pos = 1;
   } else {
      // pick the first one randomly, 0..items-1
      it1pos = random() % rankings.size;

      // pick the second one randomly out of what's left
      it2pos = random() % rankings.size-1;
      // if it2pos >= it1pos then shift it up by one
      //    so it doesn't duplicate the first item chosen
      if (it2pos >= it1pos) {
         it2pos++;
      }

   }
   // present user with choice, get resulting score
   ItemRank *item1 = getNthItem(rankings, it1pos);
   ItemRank *item2 = getNthItem(rankings, it2pos);
   float scoreX = getChoice(item1, item2);
   float scoreY = 1 - scoreX;

   // get new rankings
   int newXrank = calcNewRank(item1->rank, item2->rank, scoreX);
   int newYrank = calcNewRank(item2->rank, item1->rank, scoreY);

   // in debugging mode display the old and new ranks
   if (DEBUGMODE) {
      cerr << "DEBUG INFO:" << endl;
      cerr << "   old x rank " << item1->rank << ", new rank " << newXrank << endl;
      cerr << "   old y rank " << item2->rank << ", new rank " << newYrank << endl;
   }

   // update the rankings
   item1->rank = newXrank;
   item2->rank = newYrank;
   return true;
}

float getChoice(ItemRank* X, ItemRank* Y)
{
   if ((!X) || (!Y)) {
      cerr << "Error: passed null item to getChoice, treating as draw" << endl;
      return 0.5;
   }
   cout << endl;
   cout << "Given the choice between the following two items:" << endl;
   cout << "   First:  " << X->description << endl;
   cout << "   Second: " << Y->description << endl;
   bool validChoice;
   char choice;
   do {
      validChoice = false;
      cout << "please enter F for first, S for second, or D for draw (tied)" << endl;
      cin >> choice;
      choice = toupper(choice);
      if ((choice == 'F') || (choice == 'S') || (choice == 'D')) {
         validChoice = true;
      } else {
         cerr << "Error, " << choice << " is not a valid response, ";
         cerr << "please try again" << endl;
      }
   } while (!validChoice);
   if (choice == 'F') {
      return 1;
   } else if (choice == 'S') {
      return 0;
   } else {
      return 0.5;
   }
}

int calcNewRank (int rankX, int rankY, float score)
{
   if (rankX < 0) {
      cerr << "Error: invalid (negative) ranking passed, " << rankX;
      cerr << ", no change applied" << endl;
      return rankX;
   } else if (rankY < 0) {
      cerr << "Error: invalid (negative) ranking passed, " << rankY;
      cerr << ", no change applied" << endl;
      return rankX;
   } else if ((score != 1) && (score != 0.5) && (score != 0)) {
      cerr << "Error: invalid score passed (not 1, 0.5, 0), " << score;
      cerr << ", no change applied" << endl;
      return rankX;
   }

   // calculate expected score, probability of X win (range 0..1)
   float expected = 1 / (1 + pow(10, (rankY - rankX)/400.0));

   // calculate change to X's rank
   int change = round(16 * (score - expected));
   if (change == 0) {
      cerr << "**Note, negligible score change ignored: " << (16*(score-expected)) << endl;
   }

   // return new X rank
   return (rankX + change);
}

void msortRanks(ItemList &rankings)
{
   // quit when down to 0 or 1 elements, i.e. front == back
   if (rankings.front == rankings.back) {
      return;
   }

   // split rankings into two equal lists (off by 1 for odd-sized rankings list)
   // create two empty lists to hold the halves
   ItemList source1;
   initList(source1);
   ItemList source2;
   initList(source2);
   bool even = true; // keeps track of which source to insert into next
   while (rankings.front) {
      ItemRank* moving = removeFront(rankings);
      moving->next = nullptr;
      moving->prev = nullptr;
      if (even) {
        if (!insertBack(source1, moving)) {
           cerr << "Error; insertion failure during mergesort splitting" << endl;
           return;
        }
      } else {
        if (!insertBack(source2, moving)) {
           cerr << "Error; insertion failure during mergesort splitting" << endl;
           return;
        }
      }
      // switch between even/odd
      even = !even;
   }

   // sort the two halves then merge the results back together
   msortRanks(source1);
   msortRanks(source2);
   merge(source1, source2, rankings);
}

ItemRank* getNthItem(ItemList rankings, int N)
{
   int count = 0;
   ItemRank *current = rankings.front;
   while (count < N) {
      if (current == nullptr) {
         cerr << "Error: ran into end of list trying to get item in position " << N << endl;
         return nullptr;
      }
      current = current->next;
      count++;
   }
   return current;
}

void initList(ItemList &rankings)
{
   rankings.front = nullptr;
   rankings.back = nullptr;
   rankings.size = 0;
}

ItemRank* removeFront(ItemList &rankings)
{
   // grab the front item, check list wasn't empty
   ItemRank* current = rankings.front;
   if (!current) {
      cerr << "Error: attempted to remove from empty list" << endl;
      return current;
   }

   // detach item from list, accounting for case where it was the only item in the list
   rankings.front = rankings.front->next;
   if (rankings.front) {
      rankings.front->prev = nullptr;
   } else {
      rankings.back = nullptr;
   }
   current->prev =  nullptr;
   current->next = nullptr;

   // list's size has shrunk by one
   rankings.size--;
   return current;
}

bool insertBack(ItemList &rankings, ItemRank *item)
{
   if (!item) {
      cerr << "Error: attempting to insert a null item" << endl;
      return false;
   }
   if (!rankings.front) {
      // inserting into empty list
      item->prev = nullptr;
      rankings.front = item;
      rankings.back = item;
      rankings.size = 1;
   } else {
      // inserting at back of nonempty list
      item->prev = rankings.back;
      rankings.back->next = item;
      rankings.back = item;
      rankings.size++;
   }
   item->next = nullptr;
   return true;
}

bool merge(ItemList &source1, ItemList &source2, ItemList &destination)
{
   ItemRank* current;
   while (source1.front && source2.front) {
      // i.e. both lists still have content
      if (source1.front->rank <= source2.front->rank) {
         current = removeFront(source1);
      } else {
         current = removeFront(source2);
      }
      if (!current || !insertBack(destination, current)) {
         cerr << "Error attempting to move item from sources during merge" << endl;
         return false;
      }
   }

   // might still be stuff left over in one list or the other, grab what's left

   while (source1.front) {
      current = removeFront(source1);
      if (!current || !insertBack(destination, current)) {
         cerr << "Error attempting to move item from source1 during merge" << endl;
         return false;
      }
   }

   while (source2.front) {
      current = removeFront(source2);
      if (!current || !insertBack(destination, current)) {
         cerr << "Error attempting to move item from source2 during merge" << endl;
         return false;
      }
   }

   return true;
}

