// See License.txt for copyright and terms of use

#include "squash_patcher.h"
#include "squash_min_max_line_visitor.h"
#include <fstream>
#include <ios>
#include <sstream>
#include <map>
#include "squash_cmd.h"
#include "squash_util.h"

Patcher::Patcher(SquashCmd &cmd):cmd(cmd) {
  
}

Patcher::~Patcher() {
  flush();
}

void Patcher::flush() {
  // process renames, stuff them into the output queue
  // first loop through files
  for (Renames::iterator rit = outputRenames.begin(); rit != outputRenames.end();
      rit++) 
  {
    std::string const &file = rit->first;
    load(file);
    //make an alias to avoid redundant queries
    std::map<unsigned int, std::string> &currentOutput = output[file];
    RenamesByLine::iterator renamesByLineIt = rit->second.begin();
    // now loop through replacements by line
    for (; renamesByLineIt != rit->second.end(); renamesByLineIt++) {
      int const lineNum = renamesByLineIt->first;
      std::string const &origLine = lines[lineNum - 1]; 
      std::string line = origLine;
      int minCol = 0;
      // this adjusts replacements to compensate for macro expansion
      int adjustment = 0;
      // now consecutively perform replacement in the line
      for(RenamesByColumn::iterator cit = renamesByLineIt->second.begin();
          cit != renamesByLineIt->second.end(); cit++)
      {
        const int suggestedMinCol = cit->first + adjustment;
        minCol = replaceWord(file, lineNum, minCol, suggestedMinCol,
                             cit->second.first, cit->second.second, line, line);
        // if minCol < suggestedMinCol next replacement must be adjusted -vly
        // if minCol > suggestedMinCol next replacement must be adjusted +vly
        adjustment += minCol - suggestedMinCol;
      }
      if (currentOutput.find(lineNum) != currentOutput.end()) {
        cerr << "Pretty printed patch and string substitution requested on the "
             << "same line in file '" << file << "' on line " << lineNum 
             << ". existing content:";
        cerr << currentOutput[lineNum] << endl;
        exit(1);
      }
      MinMaxLineVisitor mmlv;
      mmlv.minLine = mmlv.maxLine = lineNum;
      printPatch(line+ "\n", mmlv, file);
    }
  }
  for (patch_map::const_iterator files =
     output.begin();
       files != output.end();files++)
  {
    std::cout << "--- " << files->first << "\n";
    std::cout << "+++ " << files->first << "\n";
    std::map<unsigned int, std::string>::const_iterator hunks =
        files->second.begin();
    for (;hunks != files->second.end();hunks++) {
      std::cout << hunks->second;
    }
  } 
}

void Patcher::load(std::string const &inFile) {
  std::string const file = resolveAbsolutePath(inFile);
  if (this->file == file)
    return;

  this->file = file;
  lines.clear();

  std::ifstream f(file.c_str());
  if (!f.is_open())
  {
    cerr << "Could not open " << file << endl;
    exit(1);
  }
  std::string line;
  while (std::getline(f, line)) {
    lines.push_back(line);
  }
  f.close();
}

void Patcher::copy(MinMaxLineVisitor const &mmlv, std::string const &file,
                    std::ostream &ostream, std::string const &prefix)
{
  load(file);
  xassert(mmlv.maxLine <= lines.size());
  for (unsigned int i = mmlv.minLine;i <= mmlv.maxLine;i++) {
    ostream << prefix << lines[i-1] << "\n";
  }
}

void Patcher::printHunkHeaderAndDeletedLines(MinMaxLineVisitor const &mmlv,
                                             int added_lines,
                                             std::string const &file,
                                             std::ostream &ostream)
{
  ostream << "@@"
  << " -" << mmlv.minLine << "," << ((mmlv.maxLine - mmlv.minLine) + 1)
  << " +" << mmlv.minLine << "," << added_lines
  << " @@\n";

  copy(mmlv, file, ostream, "-");
}

std::string Patcher::getLine(unsigned int line, std::string const &file) {
  load(file);
  xassert(line <= lines.size());
  return lines[line - 1];
}

std::string Patcher::getRange(MinMaxLineVisitor const &mmlv) {
  load(mmlv.getFile());
  xassert(mmlv.maxLine <= lines.size());
  std::string str;
  for (unsigned int i = mmlv.minLine; i <= mmlv.maxLine; i++) {
    unsigned int offset = 0;
    std::string line = lines[i - 1];
    if (i == mmlv.minLine) {
      offset = mmlv.getIndent();
      line.erase(0, offset);
    }
    if (i != mmlv.maxLine) {
      line.append("\n");
    } else if (mmlv.maxCol) {
      line.erase(mmlv.maxCol - offset);
    }
    str += line;
  }
  return str;
}

static int countNewlines(std::string const &str) {
  //if string ends with a newline, don't count it
  int count = 0;
  for (std::string::size_type n = str.find('\n');
      n != std::string::npos;
      n = str.find('\n', n + 1))
  {
    count++;
  }
  return count;
}

void Patcher::printPatch(const std::string &str,
                          const MinMaxLineVisitor &mmlv,
                          std::string const &file)
{
  int lines = countNewlines(str);
  std::stringstream ss;
  printHunkHeaderAndDeletedLines(mmlv, lines, file, ss);
  std::string::size_type n = 0;
  std::string patchPrefix(1, ' ');
  patchPrefix[0] = '+';

  if (lines) {
    do {
      std::string::size_type pos = str.find('\n', n);
      // if str contains patch lines, let them be
      const std::string prefix = str.size() > n
           && str[n] == '+' ? "" : patchPrefix;
      if (pos == std::string::npos) {
        pos = str.size();
      }
      ss << prefix << str.substr(n, pos - n) << endl;
      n = pos + 1;
    } while(n < str.size());
  }
  output[resolveAbsolutePath(file)][mmlv.minLine] = ss.str();
}

unsigned int Patcher::lineCount(std::string const &file) {
  load(file);
  return lines.size();
}

std::string Patcher::resolveAbsolutePath(std::string const &inPath) const {
  std::string path = ::resolveAbsolutePath(dir, inPath);
  for (str_map::iterator it = cmd.pathSubstitutions.begin();
      it != cmd.pathSubstitutions.end(); it++ ) 
  {
    std::string const &prefix = it->first;
    if (prefix.size() > path.size()) continue;
    
    if (prefix != path.substr(0, prefix.size())) continue;
    // still need to figure out symlinks
    path = ::resolveAbsolutePath(dir,it->second + path.substr(prefix.size(),
                                                  path.size() - prefix.size()));
    break;
  }
  return path;
}

void Patcher::setFile(std::string const &file) {
  dir = "";
  if (file.empty() ) return;
  std::string::size_type pos = file.rfind('/');
  if (pos != std::string::npos) {
    dir = file.substr(0, pos + 1);
  }
}

// Return index in dest starting after the end of with word
int Patcher::replaceWord(std::string const &file, int srclineNum, int minCol,
                         int suggestedCol, std::string const &what,
                         std::string const &with, std::string const srcline,
                         std::string &dest) {
  std::string::size_type pos = srcline.find(what, suggestedCol);
  // cpp makes life suck: could've sent us too far due to macro expansion
  // i hate heuristics like this
  if (pos == std::string::npos) {
    pos = srcline.find(what, minCol);
  }
  if (pos == std::string::npos) {
    if (minCol && srcline.find("PR_MAX") != std::string::npos) {
      return minCol;
    }
    cerr << "Could not replace '" << what << "' at column  "<<suggestedCol
    <<"(minCol="<<minCol<<")"<<" in: " << srcline << endl
    <<" Error at " << file << ":" << srclineNum <<endl;
    exit(1);
  }
  dest = srcline;
  dest.replace(pos, what.size(), with);
  return pos + with.size();
}

void Patcher::replaceWord(MinMaxLineVisitor const &mmlv, 
                          std::string const &what, std::string const &with) {
  std::string const file = resolveAbsolutePath(mmlv.getFile());
  unsigned int minLine = mmlv.minLine;
  // compensate for multiline preprocessor argument braindamage
  for (;minLine < mmlv.maxLine;minLine++) {
    if (getLine(minLine, file).find(what) != std::string::npos) break;
  }
  outputRenames[file][minLine][mmlv.getIndent()] = strpair(what, with);
}
