/*
   Copyright 2007-2008 David Nolden <david.nolden.kdevelop@art-master.de>

   This library is free software; you can redistribute it and/or
   modify it under the terms of the GNU Library General Public
   License version 2 as published by the Free Software Foundation.

   This library 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
   Library General Public License for more details.

   You should have received a copy of the GNU Library General Public License
   along with this library; see the file COPYING.LIB.  If not, write to
   the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
   Boston, MA 02110-1301, USA.
*/

#include "cpputils.h"

#include "setuphelpers.h"
#include "parser/rpp/preprocessor.h"
#include "includepathcomputer.h"

#include <interfaces/icore.h>
#include <interfaces/iprojectcontroller.h>
#include <interfaces/iproject.h>

#include <language/codegen/coderepresentation.h>
#include <language/duchain/declaration.h>
#include <language/duchain/duchain.h>
#include <language/duchain/duchainlock.h>
#include <language/util/includeitem.h>

#include "interfaces/foregroundlock.h"

#include <project/projectmodel.h>

#include <QDirIterator>
#include <QThread>
#include <QCoreApplication>

template<class T>
static QList<T> makeListUnique(const QList<T>& list)
{
  QList<T> ret;

  QSet<QString> set;
  foreach(const T& item, list)
  {
    if(!set.contains(item))
    {
      ret << item;
      set.insert(item);
    }
  }
  
  return ret;
}

QString addDot(QString ext) {
  if(ext.contains('.')) //We need this check because of the _impl.h thing
    return ext;
  else
    return "." + ext;
}

using namespace KDevelop;

namespace CppUtils
{
  
int findEndOfInclude(QString line)
{
  QString tmp = line;
  tmp = tmp.trimmed();
  if(!tmp.startsWith("#"))
    return -1;
  
  tmp = tmp.mid(1).trimmed();

  if(!tmp.startsWith("include"))
    return -1;
  
  return line.indexOf("include") + 7;
}

QString sourceOrHeaderCandidate( const QString &path_, bool fast )
{
// get the path of the currently active document
  QFileInfo fi( path_ );
  QString path = fi.filePath();
  // extract the exension
  QString ext = fi.suffix();
  if ( ext.isEmpty() )
    return QString();
  // extract the base path (full path without '.' and extension)
  QString base = path.left( path.length() - ext.length() - 1 );
  //kDebug( 9007 ) << "base: " << base << ", ext: " << ext << endl;
  // just the filename without the extension
  QString fileNameWoExt = fi.fileName();
  if ( !ext.isEmpty() )
    fileNameWoExt.replace( "." + ext, "" );
  QStringList possibleExts;
  // depending on the current extension assemble a list of
  // candidate files to look for
  QStringList candidates;
  // special case for template classes created by the new class dialog
  if ( path.endsWith( "_impl.h" ) )
  {
    QString headerpath = path;
    headerpath.replace( "_impl.h", ".h" );
    candidates << headerpath;
    fileNameWoExt.replace( "_impl", "" );
    possibleExts << "h";
  }
  // if file is a header file search for implementation file
  else if ( headerExtensions().contains( ext ) )
  {
    foreach(const QString& ext, sourceExtensions())
      candidates << ( base + addDot(ext) );

    possibleExts = sourceExtensions();
  }
  // if file is an implementation file, search for header file
  else if ( sourceExtensions().contains( ext ) )
  {
    foreach(const QString& ext, headerExtensions())
      candidates << ( base + addDot(ext) );

    possibleExts = headerExtensions();
  }
  // search for files from the assembled candidate lists, return the first
  // candidate file that actually exists or QString::null if nothing is found.
  QStringList::ConstIterator it;
  for ( it = candidates.constBegin(); it != candidates.constEnd(); ++it )
  {
//     kDebug( 9007 ) << "Trying " << ( *it ) << endl;
    if ( QFileInfo( *it ).exists() )
    {
//       kDebug( 9007 ) << "using: " << *it << endl;
      return * it;
    }
  }

  if(fast)
    return QString();

  //kDebug( 9007 ) << "Now searching in project files." << endl;
  // Our last resort: search the project file list for matching files

  QFileInfo candidateFileWoExt;
  QString candidateFileWoExtString;

  const IndexedString file(path);
  foreach (KDevelop::IProject *project, ICore::self()->projectController()->projects()) {
      if (project->inProject(file)) {
        foreach(const IndexedString& otherFile, project->fileSet()) {
          candidateFileWoExt.setFile(otherFile.str());
          //kDebug( 9007 ) << "candidate file: " << otherFile.str() << endl;
          if( !candidateFileWoExt.suffix().isEmpty() )
            candidateFileWoExtString = candidateFileWoExt.fileName().replace( "." + candidateFileWoExt.suffix(), "" );

          if ( candidateFileWoExtString == fileNameWoExt )
          {
            if ( possibleExts.contains( candidateFileWoExt.suffix() ) || candidateFileWoExt.suffix().isEmpty() )
            {
              //kDebug( 9007 ) << "checking if " << url << " exists" << endl;
              return otherFile.str();
            }
          }
        }
      }
  }

  return QString();
}


bool isHeader(const KUrl &url) {
  QFileInfo fi( url.toLocalFile() );
  QString path = fi.filePath();
  // extract the exension
  QString ext = fi.suffix();
  if ( ext.isEmpty() )
    return true;
  
  return headerExtensions().contains(ext);
}

const Cpp::ReferenceCountedMacroSet& standardMacros()
{
  static Cpp::ReferenceCountedMacroSet macros = CppTools::setupStandardMacros();
  return macros;
}

QPair<Path, Path> findInclude(const Path::List& includePaths, const Path& localPath,
                              const QString& includeName, int includeType,
                              const Path& skipPath, bool quiet){
    QPair<Path, Path> ret;
#ifdef DEBUG
    kDebug(9007) << "searching for include-file" << includeName;
    if( !skipPath.isEmpty() )
        kDebug(9007) << "skipping path" << skipPath;
#endif

    if (includeName.startsWith('/')) {
        QFileInfo info(includeName);
        if (info.exists() && info.isReadable() && info.isFile()) {
            //kDebug(9007) << "found include file:" << info.absoluteFilePath();
            ret.first = Path(info.canonicalFilePath());
            ret.second = Path("/");
            return ret;
        }
    }

    if (includeType == rpp::Preprocessor::IncludeLocal && localPath != skipPath) {
        Path check(localPath, includeName);
        QFileInfo info(check.toLocalFile());
        if (info.exists() && info.isReadable() && info.isFile()) {
            //kDebug(9007) << "found include file:" << info.absoluteFilePath();
            ret.first = check;
            ret.second = localPath;
            return ret;
        }
    }

    //When a path is skipped, we will start searching exactly after that path
    bool needSkip = skipPath.isValid();

restart:
    foreach( const Path& path, includePaths ) {
        if( needSkip ) {
            if( path == skipPath ) {
                needSkip = false;
                continue;
            }
        }

        Path check(path, includeName);
        QFileInfo info(check.toLocalFile());

        if (info.exists() && info.isReadable() && info.isFile()) {
            //kDebug(9007) << "found include file:" << info.absoluteFilePath();
            ret.first = check;
            ret.second = path;
            return ret;
        }
    }

    if( needSkip ) {
        //The path to be skipped was not found, so simply start from the begin, considering any path.
        needSkip = false;
        goto restart;
    }

    const int idx = includeName.indexOf('/');
    if ( idx != -1 ) {
      // HACK: parse Qt4 includes and similar even without the full include paths from the project manager
      // there, a file in /usr/include/qt4/QtCore/ tries to include sibling files via QtCore/file
      ret = findInclude(includePaths, localPath, includeName.mid(idx + 1), rpp::Preprocessor::IncludeLocal, skipPath, false);
    }

    if( !ret.first.isValid())
    {
        //Check if there is an available artificial representation
        if(!includeName.isNull() && artificialCodeRepresentationExists(IndexedString(includeName)))
        {
            ret.first = Path(CodeRepresentation::artificialPath(includeName));
            kDebug() << "Utilizing Artificial code for include: " << includeName;
        }
        else if(!quiet ) {
            kDebug() << "FAILED to find include-file" << includeName << "in paths:" << includePaths;
        }
    }
    
    return ret;
}

bool needsUpdate(const Cpp::EnvironmentFilePointer& file, const Path& localPath, const Path::List& includePaths)
{
  if(file->needsUpdate())
    return true;

  ///@todo somehow this check should also go into EnvironmentManager
  for( Cpp::ReferenceCountedStringSet::Iterator it = file->missingIncludeFiles().iterator(); it; ++it ) {

    ///@todo store IncludeLocal/IncludeGlobal somewhere
    auto included = findInclude( includePaths, localPath, (*it).str(), rpp::Preprocessor::IncludeLocal, Path(), true );
    if(included.first.isValid())
      return true;
  }

  return false;
}

/**
 * This is an ugly HACK but well... it works and doesn't require an extremely big API refactoring.
 *
 * Thing is, the findIncludePaths was potentially called from background threads, mostly during
 * code completion. From there, it is not safe to call IncludePathComputer::computeForeground.
 * To fix this, we now add another eventloop roundtrip, execute that function in the foreground
 * and then afterwards compute the rest in the background. This does mean that the background thread
 * is _blocked_ while we compute stuff in the foreground.
 *
 * This must be properly fixed when we port the MissingInclude feature to clang. There, the
 * include paths must be queried in the foreground thread, before invoking code completion.
 */
class IncludePathForegroundComputer : public DoInForeground
{
public:
    IncludePathForegroundComputer(IncludePathComputer* includePathComputer)
      : m_includePathComputer(includePathComputer)
    {}

private:
    void doInternal() override final
    {
      m_includePathComputer->computeForeground();
    }

    IncludePathComputer* m_includePathComputer;
};

Path::List findIncludePaths(const QString& source)
{
  Q_ASSERT(QThread::currentThread() == qApp->thread() ||
           (!DUChain::lock()->currentThreadHasReadLock() && !DUChain::lock()->currentThreadHasWriteLock()));

  IncludePathComputer comp(source);
  IncludePathForegroundComputer foreground(&comp);
  foreground.doIt();
  comp.computeBackground();
  return comp.result();
}

QList<KDevelop::IncludeItem> allFilesInIncludePath(const QString& source, bool local, const QString& addPath,
                                                   const QStringList& addIncludePaths, bool onlyAddedIncludePaths,
                                                   bool prependAddedPathToName, bool allowSourceFiles)
{
    QList<KDevelop::IncludeItem> ret;

    QStringList paths;
    if ( addPath.startsWith('/') ) {
      paths << QString("/");
    } else {
      paths = addIncludePaths;
      if(!onlyAddedIncludePaths) {
        foreach(const Path& path, findIncludePaths(source)) {
          paths += path.toLocalFile();
        }

        if(local) {
            KUrl localPath(source);
            localPath.setFileName(QString());
            paths.push_front(localPath.toLocalFile());
        }
      }
    }

    paths = makeListUnique<QString>(paths);
    int pathNumber = 0;

    QSet<QString> hadIncludePaths;
    foreach(const QString& path, paths)
    {
        if(hadIncludePaths.contains(path)) {
          continue;
        }
        hadIncludePaths.insert(path);
        QString searchPath = path;
        if (!addPath.isEmpty() && !addPath.startsWith('/')) {
          if (!searchPath.endsWith('/')) {
            searchPath += '/';
          }
          searchPath += addPath;
        }

        QDirIterator dirContent(searchPath);

        while(dirContent.hasNext()) {
             dirContent.next();
            KDevelop::IncludeItem item;
            item.name = dirContent.fileName();

            if(item.name.startsWith('.') || item.name.endsWith("~")) //This filters out ".", "..", and hidden files, and backups
              continue;
            QString suffix = dirContent.fileInfo().suffix();
            if(!dirContent.fileInfo().suffix().isEmpty() && !headerExtensions().contains(suffix) && (!allowSourceFiles || !sourceExtensions().contains(suffix)))
              continue;
            
            QString fullPath = dirContent.fileInfo().canonicalFilePath();
            if (hadIncludePaths.contains(fullPath)) {
              continue;
            } else {
              hadIncludePaths.insert(fullPath);
            }
            if(prependAddedPathToName) {
              item.name = addPath + item.name;
              item.basePath = path;
            } else {
              item.basePath = searchPath;
            }
            
            item.isDirectory = dirContent.fileInfo().isDir();
            item.pathNumber = pathNumber;

            ret << item;
        }
        ++pathNumber;
    }

    return ret;
}

QStringList headerExtensions()
{
  static const QStringList headerExtensions = QString("h,H,hh,hxx,hpp,tlh,h++").split(',');
  return headerExtensions;
}

QStringList sourceExtensions()
{
  static const QStringList sourceExtensions = QString("c,cc,cpp,c++,cxx,C,m,mm,M,inl,_impl.h").split(',');
  return sourceExtensions;
}

}
