/***************************************************************************
   copyright            : (C) 2007 by David Nolden
   email                : david.nolden.kdevelop@art-master.de
***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   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.                                   *
 *                                                                         *
 ***************************************************************************/

/** Compatibility:
 * make/automake: Should work perfectly
 * cmake: Thanks to the path-recursion, this works with cmake(tested with version "2.4-patch 6" tested with tdelibs out-of-source and with tdevelop4 in-source)
 *
 * 
 * unsermake:
 * unsermake is detected by reading the first line of the makefile. If it contains "generated by unsermake" the following things are respected:
 * 1. Since unsermake does not have the -W command(which should tell it to recompile the given file no matter whether it has been changed or not), the file-modification-time of the file is changed temporarily and the --no-real-compare option is used to force recompilation.
 * 2. The targets seem to be called *.lo instead of *.o when using unsermake, so *.lo names are used.
 * example-(test)command: unsermake --no-real-compare -n myfile.lo
 **/

#include <stdio.h>
#include <unistd.h>
#include <memory>
#include "kurl.h" /* defines KURL */
#include "tqdir.h" /* defines TQDir */
#include "tqregexp.h" /* defines TQRegExp */
#include "tdelocale.h" /* defines [function] i18n */
#include "blockingkprocess.h" /* defines BlockingTDEProcess */
#include "includepathresolver.h"
#include <sys/stat.h>
#include <sys/time.h>
#include <time.h>
#include <stdlib.h>

#ifdef TEST
#include "blockingkprocess.cpp"

#include <iostream>
using namespace std;
#endif

#ifndef TEST
#define ifTest(x) {}
#else
#define ifTest(x) x
#endif

///After how many seconds should we retry?
#define CACHE_FAIL_FOR_SECONDS 200

using namespace CppTools;


namespace CppTools {
  ///Helper-class used to fake file-modification times
  class FileModificationTimeWrapper {
    public:
      ///@param files list of files that should be fake-modified(modtime will be set to current time)
      FileModificationTimeWrapper( const TQStringList& files = TQStringList() ) : m_newTime( time(0) ) {
        for( TQStringList::const_iterator it = files.begin(); it != files.end(); ++it ) {
          ifTest( cout << "touching " << (*it).ascii() << endl );
          struct stat s;
          if( stat( (*it).local8Bit().data(), &s ) == 0 ) {
            ///Success
            m_stat[*it] = s;
            ///change the modification-time to m_newTime
            struct timeval times[2];
            times[0].tv_sec = m_newTime;
            times[0].tv_usec = 0;
            times[1].tv_sec = m_newTime;
            times[1].tv_usec = 0;
            
            if( utimes( (*it).local8Bit().data(), times ) != 0 )
            {
              ifTest( cout << "failed to touch " << (*it).ascii() << endl );
            }
          }
        }
      }

      //Not used yet, might be used to return LD_PRELOAD=.. FAKE_MODIFIED=.. etc. later
      TQString commandPrefix() const {
        return TQString();
      }

      ///Undo changed modification-times
      void unModify() {
        for( StatMap::const_iterator it = m_stat.begin(); it != m_stat.end(); ++it ) {

          ifTest( cout << "untouching " << it.key().ascii() << endl );
          
          struct stat s;
          if( stat( it.key().local8Bit().data(), &s ) == 0 ) {
            if( s.st_mtime == m_newTime ) {
              ///Still the modtime that we've set, change it back
              struct timeval times[2];
              times[0].tv_usec = 0;
              times[0].tv_sec = s.st_atime;
              times[1].tv_usec = 0;
              times[1].tv_sec = (*it).st_mtime;
              if( utimes( it.key().local8Bit().data(), times ) != 0 ) {
                ifTest( cout << "failed to untouch " << it.key().ascii() << endl );
              }
            } else {
              ///The file was modified since we changed the modtime
              ifTest( cout << " will not untouch " << it.key().ascii() << " because the modification-time has changed" << endl );
            }
          }
        }
      };
      
      ~FileModificationTimeWrapper() {
        unModify();
      }
      
    private:
    typedef TQMap<TQString, struct stat> StatMap;
    StatMap m_stat;
    time_t m_newTime;
  };

  class SourcePathInformation {
    public:
    SourcePathInformation( const TQString& path ) : m_path( path ), m_isUnsermake(false), m_shouldTouchFiles(false) {
      m_isUnsermake = isUnsermakePrivate( path );
      
      ifTest( if( m_isUnsermake ) cout << "unsermake detected" << endl );
    }

    bool isUnsermake() const {
      return m_isUnsermake;
    }

    ///When this is set, the file-modification times are changed no matter whether it is unsermake or make
    void setShouldTouchFiles(bool b) {
      m_shouldTouchFiles = b;
    }

    TQString getCommand( const TQString& sourceFile, const TQString& makeParameters ) const {
      if( isUnsermake() )
        return "unsermake -k --no-real-compare -n " + makeParameters;
      else
        return "make -k --no-print-directory -W \'" + sourceFile + "\' -n " + makeParameters;
    }

    bool hasMakefile() const {
        TQFileInfo makeFile( m_path, "Makefile" );
        return makeFile.exists();
    }

    bool shouldTouchFiles() const {
      return isUnsermake() || m_shouldTouchFiles;
    }
    
    TQStringList possibleTargets( const TQString& targetBaseName ) const {
      TQStringList ret;
      if( isUnsermake() ) {
	//unsermake breaks if the first given target does not exist, so in worst-case 2 calls are necessary
	ret << targetBaseName + ".lo";
	ret << targetBaseName + ".o";
      } else {
	//It would be nice if both targets could be processed in one call, the problem is the exit-status of make, so for now make has to be called twice.
	ret << targetBaseName + ".o";
	ret << targetBaseName + ".lo";
	//ret << targetBaseName + ".lo " + targetBaseName + ".o";
      }
      return ret;
    }

    private:
      bool isUnsermakePrivate( const TQString& path ) {
        bool ret = false;
        TQFileInfo makeFile( path, "Makefile" );
        TQFile f( makeFile.absFilePath() );
        if( f.open( IO_ReadOnly ) ) {
          TQString firstLine;
          f.readLine( firstLine, 1000 );
          if( firstLine.find( "generated by unsermake" ) != -1 ) {
            ret = true;
          }
          f.close();
        }
        return ret;
      }

      TQString m_path;
      bool m_isUnsermake;
      bool m_shouldTouchFiles;
  };
  
};

bool IncludePathResolver::executeCommandPopen ( const TQString& command, const TQString& workingDirectory, TQString& result ) const
{
  ifTest( cout << "executing " << command.ascii() << endl );
    
  char* oldWd = getcwd(0,0);
  chdir( workingDirectory.local8Bit() );
  
  FILE* fp;
  const int BUFSIZE = 2048;
  char buf [BUFSIZE];

  result = TQString();

  int status = 1;
  if ((fp = popen(command.local8Bit(), "r")) != NULL) {
    while (fgets(buf, sizeof (buf), fp))
      result += TQString(buf);

    status = pclose(fp);
  }

  if( oldWd ) {
    chdir( oldWd );
    free( oldWd );
  }
  return status == 0;
}

IncludePathResolver::IncludePathResolver( bool continueEventLoop ) : m_isResolving(false), m_outOfSource(false), m_continueEventLoop(continueEventLoop)  {
/*  m_continueEventLoop = false;
#warning DEBUGGING TEST, REMOVE THIS*/
}

///More efficient solution: Only do exactly one call for each directory. During that call, mark all source-files as changed, and make all targets for those files.
PathResolutionResult IncludePathResolver::resolveIncludePath( const TQString& file ) {
  TQFileInfo fi( file );
  return resolveIncludePath( fi.fileName(), fi.dirPath(true) );
}

PathResolutionResult IncludePathResolver::resolveIncludePath( const TQString& file, const TQString& workingDirectory ) {
  
  struct Enabler {
    bool& b;
    Enabler( bool& bb ) : b(bb) {
      b = true;
    }
    ~Enabler() {
      b = false;
    }
  };

  if( m_isResolving )
    return PathResolutionResult(false, i18n("tried include-path-resolution while another resolution-process was still running") );
  
  Enabler e( m_isResolving );
  
  ///STEP 1: CACHING
  TQDir dir( workingDirectory );
  dir = TQDir( dir.absPath() );
  TQFileInfo makeFile( dir, "Makefile" );
  if( !makeFile.exists() )
    return PathResolutionResult(false, i18n("Makefile is missing in folder \"%1\"").arg(dir.absPath()), i18n("problem while trying to resolve include-paths for %1").arg(file) );

  TQStringList cachedPath; //If the call doesn't succeed, use the cached not up-to-date version
  TQDateTime makeFileModification = makeFile.lastModified();
  Cache::iterator it = m_cache.find( dir.path() );
  if( it != m_cache.end() ) {
    cachedPath = (*it).path;
    if( makeFileModification == (*it).modificationTime ) {
      if( !(*it).failed ) {
        //We have a valid cached result
        PathResolutionResult ret(true);
        ret.path = (*it).path;
        return ret;
      } else {
        //We have a cached failed result. We should use that for some time but then try again. Return the failed result if: ( there were too many tries within this folder OR this file was already tried ) AND The last tries have not expired yet
        if( /*((*it).failedFiles.size() > 3 || (*it).failedFiles.find( file ) != (*it).failedFiles.end()) &&*/ (*it).failTime.secsTo( TQDateTime::currentDateTime() ) < CACHE_FAIL_FOR_SECONDS ) {
          PathResolutionResult ret(false); //Fake that the result is ok
          ret.errorMessage = i18n("Cached: ") + (*it).errorMessage;
          ret.longErrorMessage = (*it).longErrorMessage;
          ret.path = (*it).path;
          return ret;
        } else {
          //Try getting a correct result again
        }
      }
    }
  }
  
  ///STEP 1: Prepare paths
  TQString targetName;
  TQFileInfo fi( file );
  
  TQString absoluteFile = file;
  if( !file.startsWith("/") )
    absoluteFile = dir.path() + "/" + file;
  KURL u( absoluteFile );
  u.cleanPath();
  absoluteFile = u.path();
  
  int dot;
  if( (dot = file.findRev( '.' )) == -1 )
    return PathResolutionResult( false, i18n( "Filename %1 seems to be malformed" ).arg(file) );

  targetName = file.left( dot );

  TQString wd = dir.path();
  if( !wd.startsWith("/") ) {
    wd = TQDir::currentDirPath() + "/" + wd;
    KURL u( wd );
    u.cleanPath();
    wd = u.path();
  }
  if( m_outOfSource ) {
    if( wd.startsWith( m_source ) ) {
      //Move the current working-directory out of source, into the build-system
      wd = m_build + "/" + wd.mid( m_source.length() );
      KURL u( wd );
      u.cleanPath();
      wd = u.path();
    }
  }

  SourcePathInformation source( wd );
  TQStringList possibleTargets = source.possibleTargets( targetName );

  source.setShouldTouchFiles(true); //Think about whether this should be always enabled. I've enabled it for now so there's an even bigger chance that everything works.
  
  ///STEP 3: Try resolving the paths, by using once the absolute and once the relative file-path. Which kind is required differs from setup to setup.
  
  ///STEP 3.1: Try resolution using the absolute path
  PathResolutionResult res;
  //Try for each possible target
  for( TQStringList::const_iterator it = possibleTargets.begin(); it != possibleTargets.end(); ++it ) {
    res = resolveIncludePathInternal( absoluteFile, wd, *it, source );
    if( res ) break;
  }
  if( res ) {
    CacheEntry ce;
    ce.errorMessage = res.errorMessage;
    ce.longErrorMessage = res.longErrorMessage;
    ce.modificationTime = makeFileModification;
    ce.path = res.path;
    m_cache[dir.path()] = ce;

    return res;
  }


  ///STEP 3.2: Try resolution using the relative path
  TQString relativeFile = KURL::relativePath(wd, absoluteFile);
  for( TQStringList::const_iterator it = possibleTargets.begin(); it != possibleTargets.end(); ++it ) {
    res = resolveIncludePathInternal( relativeFile, wd, *it, source );
    if( res ) break;
  }
    
  if( res.path.isEmpty() )
      res.path = cachedPath; //We failed, maybe there is an old cached result, use that.
  
  if( it == m_cache.end() )
    it = m_cache.insert( dir.path(), CacheEntry() );
  
  CacheEntry& ce(*it);
  ce.modificationTime = makeFileModification;
  ce.path = res.path;
  if( !res ) {
    ce.failed = true;
    ce.errorMessage = res.errorMessage;
    ce.longErrorMessage = res.longErrorMessage;
    ce.failTime = TQDateTime::currentDateTime();
    ce.failedFiles[file] = true;
  } else {
    ce.failed = false;
    ce.failedFiles.clear();
  }
  
  return res;
}

PathResolutionResult IncludePathResolver::getFullOutput( const TQString& command, const TQString& workingDirectory, TQString& output ) const{
  if( m_continueEventLoop ) {
    BlockingTDEProcess proc;
    proc.setWorkingDirectory( workingDirectory );
    proc.setUseShell( true );
    proc << command;
    if ( !proc.start(TDEProcess::NotifyOnExit, TDEProcess::Stdout) ) {
      return PathResolutionResult( false, i18n("Could not start the make-process") );
    }

    output = proc.stdOut();
    if( proc.exitStatus() != 0 )
      return PathResolutionResult( false, i18n("make-process finished with nonzero exit-status"), i18n("output: %1").arg( output ) );
  } else {
    bool ret = executeCommandPopen(command, workingDirectory, output);
    
    if( !ret )
      return PathResolutionResult( false, i18n("make-process failed"), i18n("output: %1").arg( output ) );
  }
  return PathResolutionResult(true);
}

PathResolutionResult IncludePathResolver::resolveIncludePathInternal( const TQString& file, const TQString& workingDirectory, const TQString& makeParameters, const SourcePathInformation& source ) {

  TQString processStdout;

  TQStringList touchFiles;
  if( source.shouldTouchFiles() )
    touchFiles << file;

  FileModificationTimeWrapper touch( touchFiles );
  
  TQString fullOutput;
  PathResolutionResult res = getFullOutput( source.getCommand( file, makeParameters ), workingDirectory, fullOutput );
  if( !res )
    return res;
  
  TQRegExp newLineRx("\\\\\\n");
  fullOutput.replace( newLineRx, "" );
  ///@todo collect multiple outputs at the same time for performance-reasons
  TQString firstLine = fullOutput;
  int lineEnd;
  if( (lineEnd = fullOutput.find('\n')) != -1 )
    firstLine.truncate( lineEnd ); //Only look at the first line of output
  
  /**
   * There's two possible cases this can currently handle.
   * 1.: gcc is called, with the parameters we are searching for(so we parse the parameters)
   * 2.: A recursive make is called, within another directory(so we follow the recursion and try again) "cd /foo/bar && make -f pi/pa/build.make pi/pa/po.o
   * */


  ///STEP 1: Test if it is a recursive make-call
  TQRegExp makeRx( "\\bmake\\s" );
  int offset = 0;
  while( (offset = makeRx.search( firstLine, offset )) != -1 )
  {
    TQString prefix = firstLine.left( offset ).stripWhiteSpace();
    if( prefix.endsWith( "&&") || prefix.endsWith( ";" ) || prefix.isEmpty() )
    {
      TQString newWorkingDirectory = workingDirectory;
      ///Extract the new working-directory
      if( !prefix.isEmpty() ) {
        if( prefix.endsWith( "&&" ) )
          prefix.truncate( prefix.length() - 2 );
        else if( prefix.endsWith( ";" ) )
          prefix.truncate( prefix.length() - 1 );
        ///Now test if what we have as prefix is a simple "cd /foo/bar" call.
        if( prefix.startsWith( "cd ") && !prefix.contains( ";") && !prefix.contains("&&") ) {
          newWorkingDirectory = prefix.right( prefix.length() - 3 ).stripWhiteSpace();
          if( !newWorkingDirectory.startsWith("/") )
            newWorkingDirectory = workingDirectory + "/" + newWorkingDirectory;
          KURL u( newWorkingDirectory );
          u.cleanPath();
          newWorkingDirectory = u.path();
        }
      }
      TQFileInfo d( newWorkingDirectory );
      if( d.exists() ) {
        ///The recursive working-directory exists.
        TQString makeParams = firstLine.mid( offset+5 );
        if( !makeParams.contains( ";" ) && !makeParams.contains( "&&" ) ) {
          ///Looks like valid parameters
          ///Make the file-name absolute, so it can be referenced from any directory
          TQString absoluteFile = file;
          if( !absoluteFile.startsWith("/") )
            absoluteFile = workingDirectory +  "/" + file;
          KURL u( absoluteFile );
          u.cleanPath();
          ///Try once with absolute, and if that fails with relative path of the file
          SourcePathInformation newSource( newWorkingDirectory );
          PathResolutionResult res = resolveIncludePathInternal( u.path(), newWorkingDirectory, makeParams, newSource );
          if( res )
            return res;
          return resolveIncludePathInternal( KURL::relativePath(newWorkingDirectory,u.path()), newWorkingDirectory, makeParams , newSource );
        }else{
          return PathResolutionResult( false, i18n("Recursive make-call failed"), i18n("The parameter-string \"%1\" does not seem to be valid. Output was: %2").arg(makeParams).arg(fullOutput) );
        }
      } else {
        return PathResolutionResult( false, i18n("Recursive make-call failed"), i18n("The directory \"%1\" does not exist. Output was: %2").arg(newWorkingDirectory).arg(fullOutput) );
      }

    } else {
      return PathResolutionResult( false, i18n("Recursive make-call malformed"), i18n("Output was: %2").arg(fullOutput) );
    }

    ++offset;
    if( offset >= firstLine.length() ) break;
  }

  ///STEP 2: Search the output for include-paths
  TQRegExp validRx( "\\b([cg]\\+\\+|gcc)" );
  if( validRx.search( fullOutput ) == -1 )
    return PathResolutionResult( false, i18n("Output seems not to be a valid gcc or g++ call"), i18n("Folder: \"%1\"  Command: \"%2\" Output: \"%3\"").arg(workingDirectory).arg( source.getCommand(file, makeParameters) ).arg(fullOutput) );

  PathResolutionResult ret( true );
  ret.longErrorMessage = fullOutput;

  TQString includeParameterRx( "\\s(-I|--include-dir=|-I\\s)" ); 
  TQString quotedRx( "(\\').*(\\')|(\\\").*(\\\")" ); //Matches "hello", 'hello', 'hello"hallo"', etc.
  TQString escapedPathRx( "(([^)(\"'\\s]*)(\\\\\\s)?)*" ); //Matches /usr/I\ am \ a\ strange\ path/include

  TQRegExp includeRx( TQString( "%1(%2|%3)(?=\\s)" ).arg( includeParameterRx ).arg( quotedRx ).arg( escapedPathRx ) );
  includeRx.setMinimal( true );
  includeRx.setCaseSensitive( true );
  offset = 0;
  while( (offset = includeRx.search( fullOutput, offset )) != -1 ) {
    offset += 1; ///The previous white space
    int pathOffset = 2;
    if( fullOutput[offset+1] == '-' ) {
      ///Must be --include-dir=, with a length of 14 characters
      pathOffset = 14;
    }
    if( fullOutput.length() <= offset + pathOffset )
      break;
    
    if( fullOutput[offset+pathOffset].isSpace() )
      pathOffset++;
    
    

    int start = offset + pathOffset;
    int end = offset + includeRx.matchedLength();
    
    TQString path = fullOutput.mid( start, end-start ).stripWhiteSpace();
    if( path.startsWith( "\"") || path.startsWith( "\'") && path.length() > 2 ) {
      //probable a quoted path
      if( path.endsWith(path.left(1)) ) {
        //Quotation is ok, remove it
        path = path.mid( 1, path.length() - 2 );
      }
    }
    if( !path.startsWith("/") )
      path = workingDirectory + (workingDirectory.endsWith("/") ? "" : "/") + path;
    
    KURL u( path );
    u.cleanPath();
      
    ret.path << u.path();

    offset = end-1;
  }

  
  return ret;
}

void IncludePathResolver::setOutOfSourceBuildSystem( const TQString& source, const TQString& build ) {
  m_outOfSource = true;
  m_source = source;
  m_build = build;
}

#ifdef TEST
/** This can be used for testing and debugging the system. To compile it use
 * gcc includepathresolver.cpp -I /usr/share/qt3/include -I /usr/include/tde -I ../../lib/util -DTEST -ltdecore -g -o includepathresolver
 * */
         
int main(int argc, char **argv) {
  TQApplication app(argc,argv);
  IncludePathResolver resolver;
  if( argc < 3 ) {
    cout << "params: 1. file-name, 2. working-directory [3. source-directory 4. build-directory]" << endl;
    return 1;
  }
  if( argc >= 5 ) {
    cout << "mapping " << argv[3] << " -> " << argv[4] << endl;
    resolver.setOutOfSourceBuildSystem( argv[3], argv[4] );
  }
  PathResolutionResult res = resolver.resolveIncludePath( argv[1], argv[2] );
  cout << "success: " << res.success << "\n";
  if( !res.success ) {
    cout << "error-message: \n" << res.errorMessage << "\n";
    cout << "long error-message: \n" << res.longErrorMessage << "\n";
  }
  cout << "path: \n" << res.path.join("\n");
  return res.success;
}

#endif
