// This file is part of AspectC++.
// Copyright (C) 1999-2015  The AspectC++ developer team.
//                                                                
// This program is free software;  you can redistribute it and/or 
// modify it under the terms of the GNU General Public License as 
// published by the Free Software Foundation; either version 2 of 
// the License, or (at your option) any later version.            
//                                                                
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of 
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the  
// GNU General Public License for more details.                   
//                                                                
// You should have received a copy of the GNU General Public      
// License along with this program; if not, write to the Free     
// Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, 
// MA  02111-1307  USA                                            

#include "PathIterator.h"
#include "Config.h"
#include "ErrorStream.h"
#include "PathManager.h"
#include "RegComp.h"
#include "SysCall.h"
#include <filesystem>
#include <fstream>
#include <iostream>
#include <assert.h>

namespace ACBase {

PathManager::~PathManager() {
  for (auto wprot : write_protected_path_patterns_)
    delete wprot;
}

// Add a new source directory.
void PathManager::add_path(const std::string &source, const std::string &destination) {
  PathInfo path_info;
  if (!source.empty()) {
    std::string src = source;
    if (src.length() > 0 && src.back() != '/')
      src += '/';

    // Don't add a source directory twice.
    // Don't add a sub-directory of an existing source directory.
    for (auto &p : paths_)
      if (p.src_path.substr(0, src.length()) == src)
        return;

    // Add canonical filename representation
    if (!SysCall::make_canonical_path(source, path_info.src_path_canonical))
      return;
    // Add the source path.
    path_info.src_path = src;
    has_src_ = true; // TODO: check whether the second 'add_path' is consistent with the first
  }
  if (!destination.empty()) {
    std::string dest = destination;
    if (dest.length() > 0 && dest.back() != '/')
      dest += '/';
    // Add the destination path.
    path_info.dest_path = dest;
    has_dest_ = true;
  }
  if (!source.empty() || !destination.empty())
    paths_.push_back(path_info);
}

bool PathManager::is_newer (const std::string &file) const {
  time_t last_modified_src;
  if (!SysCall::file_modification_time(file, last_modified_src))
    return false;

  // determine the destination path of the file
  MapConstIter iter;
  if (!ACBase::PathManager::is_below (file, iter)) {
    assert (false); // if we came here, the file should be registered
    return false;
  }

  ACBase::ProjectFile &project_file = (ACBase::ProjectFile&)iter->second;
  auto dest = project_file.dest ();
  if (dest.empty()) {
    // determine the destination path
    std::ostringstream path;
    if (!get_dest_path (file, path))
      return false;
    std::string dest_path = path.str ();
    project_file.dest (dest_path.c_str ());
    dest = project_file.dest ();
  }

  bool newer = true;
  time_t last_modified_dest;
  if (SysCall::file_modification_time(dest, last_modified_dest))
    if (last_modified_src <= last_modified_dest)
      newer = false;
  
  return newer;
}

// checks if a give file (by name) is a registered file of this path manager
// In case of success (found!) the an iterator is returned, which can be
// used to access more information about the file.
bool PathManager::is_below(const std::string &file, MapConstIter &iter) const {
  // determine the canonical name (which has to exist)
  std::string abs;
  if (!SysCall::make_canonical_path(file, abs, true)) // TODO: really use 'weakly' here?
    return false;

  // search for the name and return the result
  iter = m_files.find(abs);

  // all files stored in the map belong to the project
  if (iter != m_files.end())
    return true;

  // otherwise check if any project path is a prefix of the canonical filename
  bool found = false;
  for (auto &p : paths_) {
    if (!p.src_path.empty()) {
      unsigned prefix_len = p.src_path_canonical.length();
      if (abs.substr(0, prefix_len) == p.src_path_canonical && abs[prefix_len] == '/') {
        found = true;
        break;
      }
    }
  }

  // if the file does not belong to the project, return false
  if (!found)
    return false;

  // otherwise store it in the list and return true
  std::pair<MapConstIter, bool> insert_result = m_files.insert(MapPair(abs, ProjectFile(file)));
  iter = insert_result.first;
  return true;
}

// Add a new file to the project file list with destination path
PathManager::MapConstIter PathManager::add_file(const std::string &filename, const std::string &dest_path) {
  
  std::string abs;
  if (!SysCall::make_canonical_path(filename, abs, true)) {
    assert(false);
    return m_files.end();
  }

  // insert the file with its canonical name as the key
  ProjectFile file(filename.c_str(), dest_path.c_str());
  auto insert_result = m_files.insert(MapPair(abs, file));

  // return the iterator
  return insert_result.first;
}

// Configure the project from the command line or a file.
void PathManager::configure(const Config &c) {
  const ConfOption *d = 0, *p = 0;

  unsigned num = c.Options();
  for (unsigned i = 0; i < num; i++) {
    const ConfOption *o = c.Option(i);
    bool new_p = false, new_d = false;

    if (o->name() == "-w") {
      if (o->arguments() != 1)
        continue;
      protect(o->argument(0).c_str());
    } else if (o->name() == "-p") {
      if (o->arguments() != 1)
        continue;
      new_p = true;
    } else if (o->name() == "-d") {
      if (o->arguments() != 1)
        continue;
      new_d = true;
    }

    if (new_p) {
      if (p) {
        add_path(p->argument(0).c_str(), d ? d->argument(0).c_str() : 0);
        if (d)
          d = 0;
      }
      p = o;
    }

    if (new_d) {
      if (d) {
        add_path(p ? p->argument(0).c_str() : 0, d->argument(0).c_str());
        if (p)
          p = 0;
      }
      d = o;
    }
  }

  if (p || d)
    add_path(p ? p->argument(0).c_str() : 0, d ? d->argument(0).c_str() : 0);
}

// Iterate the contents of the paths.
bool PathManager::iterate(PathIterator &iter) const {
  // a new iterator should start at the beginning
  if (iter.m_currentFile.empty()) {
    for (auto &p : paths_) {
      if (!p.src_path.empty()) {
        for (auto const& dir_entry : std::filesystem::recursive_directory_iterator(p.src_path)) {
          std::string name = dir_entry.path().string();
          if (name.length() >= 2 && name.substr(0,2) == "./")
            name = name.substr(2);
          if (iter.m_searchPattern->match(name.c_str()))
            iter.m_matchingFiles.push_back(name);
        }
      }
    }
    iter.m_fileIterator = iter.m_matchingFiles.begin();
  }

  bool result = false;
  if (iter.m_fileIterator == iter.m_matchingFiles.end()) {
    iter.m_currentFile = std::string();
  } else {
    iter.m_currentFile = *iter.m_fileIterator;
    ++(iter.m_fileIterator);
    result = true;
  }
  return result;
}

// Add a regular pattern specifying a path that has to be
// protected from writing.
void PathManager::protect(const std::string &path_pattern) {
  assert(!path_pattern.empty());
  write_protected_path_patterns_.push_back(new RegComp(path_pattern));
}

// Return true if the given file or path is protected
// from writing or if it isn't located within one of the 
// source directories, because then it's protected, too.
bool PathManager::is_protected(const std::string &path) const {
  if (path.empty())
    return false;

  // Protected by protect patterns?
  for (auto wprot : write_protected_path_patterns_)
    if (wprot->match(path))
      return true;

  // From outside the source directories?
  return !is_below(path);
}

bool PathManager::get_dest_path(const std::string &source, std::ostream &out) const {
  // determine the canonical name (which has to exist)
  std::string abs;
  if (!SysCall::make_canonical_path(source, abs, true))
    return false;
  for (auto &p : paths_) {
    if (!p.src_path.empty()) {
      if (abs.substr(0, p.src_path_canonical.length()) == p.src_path_canonical &&
          abs[p.src_path_canonical.length()] == '/') {
        std::string dir = p.dest_path;
        out << dir;
        if (dir[dir.length() - 1] != '/')
          out << "/";
        out << abs.substr(p.src_path_canonical.length() + 1);
        return true;
      }
    }
  }
  return false;
}

// save an opened file to its destination
void PathManager::save(const std::string &name, bool is_modified, const char *content) const {

  // Do not write files to protected paths or files from outside the
  // source directories.
  if (ACBase::PathManager::is_protected (name))
    return;

  // determine the destination path of the file
  MapConstIter iter;
  if (!is_below (name, iter)) {
    assert (false); // if we came here, the file should be registered
    return;
  }

  ACBase::ProjectFile &project_file = (ACBase::ProjectFile&)iter->second;
  std::string dest = project_file.dest ();
  if (dest.empty()) {
    // determine the destination path
    std::ostringstream path;
    if (!get_dest_path (name, path))
      return;
    std::string dest_path = path.str ();
    project_file.dest (dest_path.c_str ());
    dest = project_file.dest ();
  }

  // make sure that the directory for the file exists
  std::filesystem::path dest_file = dest;
  if (!SysCall::make_directory_hierarchy (dest_file.remove_filename()))
    return;

  // Check whether the file has to be updated.
  if (is_newer(name) || is_modified) {
    // Write the file to disk.
    std::ofstream out (dest, std::ios::out);
    out << content;
  }

}


} // namespace ACBase
