/***************************************************************************
**
**  This file is part of QGpCoreTools.
**
**  This library is free software; you can redistribute it and/or
**  modify it under the terms of the GNU Lesser General Public
**  License as published by the Free Software Foundation; either
**  version 2.1 of the License, or (at your option) any later version.
**
**  This file 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 Lesser General Public
**  License for more details.
**
**  You should have received a copy of the GNU Lesser General Public
**  License along with this library; if not, write to the Free Software
**  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
**
**  See http://www.geopsy.org for more information.
**
**  Created: 2004-03-10
**  Copyright: 2004-2019
**    Marc Wathelet
**    Marc Wathelet (ULg, Liège, Belgium)
**    Marc Wathelet (LGIT, Grenoble, France)
**
***************************************************************************/

#include <zlib.h>

#include "File.h"
#include "Trace.h"
#include "MemoryChecker.h"
#include "CoreApplication.h"

namespace QGpCoreTools {

  /*!
    CRC32 over the first \a maxSize bytes.
  */
  quint32 File::crc32(const QString& fileName, qint64 maxSize, bool *ok)
  {
    QFile f(fileName);
    quint32 val;
    if(f.open(QIODevice::ReadOnly)) {
      uchar * buffer=new uchar[maxSize];
      qint64 realSize=f.read(reinterpret_cast<char *>(buffer), maxSize);
      f.close();
      val=::crc32(0L, Z_NULL, 0);
      val=::crc32(val, buffer, realSize);
      delete [] buffer;
    } else {
      App::log(tr("Cannot open file '%1' for checksum computation.\n")
                       .arg(fileName));
      if(ok) {
        *ok=false;
      }
      val=0;
    }
    return val;
  }

/*!
  Expands all wildcards contained in \a fileNames
*/
QStringList File::expand(const QStringList& fileNames)
{
  // Look for wildcards in file names
  bool hasWildCards[fileNames.count()];
  bool hasOneWildCard=false;
  for (int i=fileNames.count()-1; i>=0; i--) {
    //QTextStream(stdout) << fileNames.at(i) << ::endl;
    hasWildCards[i]=fileNames.at(i).contains(QRegExp("[*?]"));
    if(hasWildCards[i]) hasOneWildCard=true;
  }
  if(!hasOneWildCard) {
    return fileNames;
  }
  QStringList expandedFileNames;
  int n=fileNames.count();
  for (int i=0; i<n; i++) {
    if(hasWildCards[i]) {
      expandedFileNames+=expand(fileNames.at(i));
    } else {
      expandedFileNames.append(fileNames.at(i));
    }
  }
  return expandedFileNames;
}

/*!
  Expands all wildcards contained in \a fileName
*/
QStringList File::expand(const QString& fileName)
{
  // Extract all parent directories of fileName
  QFileInfo fi(fileName);
  QStringList filters;
  QString path;
  forever{
    filters.append(fi.fileName());
    path=fi.path();
    if(path=="." || path=="/") break;
    fi.setFile(path);
  }
  return expand(path, filters, QString(), filters.count()-1);
}

/*!
  Expanding is based on QDir::entryList which returns the matching files only if they are directly contained
  in a directory. This implementation supports matching across several directories. \a d is the base directory
  where to start. \a filters is the list of patterns to match for each sub-directories at various levels ordered
  from the highest to the lowest.

  If initial pattern is:

    /dir1/dir2/dir*3/dir*4/file*

  \a d will be '/' and \a filters will contain:
    \li file*    ---> level 0
    \li dir*4    ---> level 1
    \li dir*3    ---> level 2
    \li dir2     ---> level 3
    \li dir1     ---> level 4

  \a levelName is the current expandation of current filter at current \a level.
  Upon startup level is equal to the number of filters-1 and levelName must be empty.
*/
QStringList File::expand(QDir d, QStringList filters, QString levelName, int level)
{
  if(!levelName.isEmpty()) {
    d.cd(levelName); // cd is here to avoid modification of d for each sub-directory
    level--;
  }
  //QTextStream(stdout) << level << " " << filters.join(",") << " " << d.absolutePath() << ::endl;
  QStringList allFiles;
  QString name=filters.at(level);
  if(name.contains(QRegExp("[*?]"))) {
    QStringList f;
    f << name;
    QStringList files=d.entryList(f, QDir::AllEntries | QDir::NoDotAndDotDot | QDir::CaseSensitive, QDir::NoSort);
    if(files.isEmpty()) {
      App::log(tr("cannot access %1\n").arg(d.absoluteFilePath(name)) );
      return allFiles;
    } else {
      if(level>0) {
        for(QStringList::iterator it=files.begin(); it!=files.end(); it++) {
          allFiles+=expand(d,filters,*it,level);
        }
      } else {
        for(QStringList::iterator it=files.begin(); it!=files.end(); it++) {
          allFiles+=d.absoluteFilePath(*it);
        }
      }
    }
    return allFiles;
  } else {
    if(level>0) {
      if(d.exists(name)) {
        return expand(d,filters, name, level);
      } else {
        App::log(tr("cannot access %1\n").arg(d.absoluteFilePath(name)) );
        return allFiles;
      }
    } else {
      allFiles+=d.absoluteFilePath(name);
      return allFiles;
    }
  }
}

/*!
  Returns a unique absolute file name in \a d forged on \a fileName and eventually _nnnn if the basename is already
  used. If \a fileName already contains "_nnnn", the value nnnn is taken as a starting point before incrementing.
*/
QString File::uniqueName(QString fileName, const QDir& d)
{
  TRACE;
  if(!d.exists(fileName)) {
    return d.absoluteFilePath(fileName);
  }
  QFileInfo fi(fileName);
  QString suffix=fi.completeSuffix();
  if(!suffix.isEmpty()) suffix="."+suffix;
  QString baseName=fi.baseName();
  // Get a unique file name, that does not exist in d
  int index;
  int i=baseName.lastIndexOf("_");
  if(i==-1)
    index=-1;
  else {
    bool ok;
    index=baseName.right(baseName.length() - i - 1).toInt(&ok);
    if( !ok)
      index=-1;
  }
  if(index==-1) {
    baseName+="_%1";
    index=1;
  } else {
    baseName=baseName.left(i)+"_%1";
    index++;
  }
  QString indexStr=QString::asprintf("%04i", index);
  while(d.exists(baseName.arg(indexStr)+suffix)) {
    index++;
    indexStr=QString::asprintf("%04i", index);
  }
  return d.absoluteFilePath(baseName.arg(indexStr)+suffix);
}

/*!
  Return the list of all files inside \a path and its subfolders.
*/
QMap<QString, QString> File::fileMap(const QDir& d)
{
  TRACE;
  QMap<QString, QString> map;
  fileMap(d, map);
  return map;
}

/*!
  Insert all files inside \a path and its subfolders in \a map.
*/
void File::fileMap(const QDir& d, QMap<QString, QString>& map)
{
  TRACE;
  QStringList dirs=d.entryList(QDir::AllDirs | QDir::NoDotAndDotDot);
  for(QStringList::iterator it=dirs.begin(); it!=dirs.end(); it++) {
    QDir subd(d);
    subd.cd(*it);
    fileMap(subd, map);
  }
  QStringList files=d.entryList(QDir::Files);
  for(QStringList::iterator it=files.begin(); it!=files.end(); it++) {
    map.insertMulti(*it, d.absoluteFilePath(*it));
  }
}

/*!
  Returns the file name (without the path).
*/
QString File::fileName(QString filePath, QChar sep)
{
  TRACE;
  int i=filePath.lastIndexOf(sep);
  if(i!=-1) {
    return filePath.right(filePath.count()-i-1);
  } else {
    return filePath;
  }
}

/*!
  Reads all file and returns its contents in a newly allocated buffer. It returns 0 if an error occured or
  if file is empty.
*/
char * File::readAll(const char * fileName)
{
  TRACE;
  QFileInfo fi(fileName);
  unsigned int fileLen=(unsigned int)fi.size();
  if(fileLen>0) {
    FILE * f=fopen(fileName,"rb");
    if(f) {
      char * buf=new char [fileLen+1];
      if(fread(buf,sizeof(char),fileLen,f)==fileLen) {
        buf[fileLen]='\0';
      } else {
        delete [] buf;
        buf=nullptr;
      }
      fclose(f);
      return buf;
    } else {
      return nullptr;
    }
  } else {
    return nullptr;
  }
}

/*!
  Returns the line read from \a f in a dynamic string buffer \a buf of initial length of \a bufLen..
  If the line read from \a f is longer than initial \a bufLen, buffer is re-allocated and expanded.
  \a bufLen contains the final length. If \a delEOL is true remaining end of line characters
  (LF and/or CR) at the end are removed.
*/
bool File::readLine(char *& buf, int& bufLen, FILE * f, bool delEOL)
{
  TRACE;
  buf[0]='\0';
  if(!fgets (buf, bufLen, f)) {
    return false;
  }
  while((int)strlen(buf)==bufLen-1) {
    int nbufLen=bufLen << 1;
    char * nbuf=new char [nbufLen];
    memmove(nbuf,buf,bufLen-1);
    delete [] buf;
    buf=nbuf;
    if(!fgets (buf+bufLen-1, bufLen+1, f)) {
      return false;
    }
    bufLen=nbufLen;
  }
  if(delEOL) {
    int n=strlen(buf)-1;
    if(buf[n]=='\n') {
      if(buf[n-1]=='\r') {
        buf[n-1]='\0';
      } else {
        buf[n]='\0';
      }
    }
  }
  return true;
}

/*!
  Overload function for convenience. Returns the line read from \a f in a QString.
*/
QString File::readLine(FILE * f, bool delEOL)
{
  TRACE;
  int bufLen=256;
  char * buf=new char[bufLen];
  File::readLine(buf, bufLen, f, delEOL);
  QString line(buf);
  delete [] buf;
  return line;
}

/*!
  Overload function for convenience. Returns the line read from stdin in a QString.
  User interrupt debug facility is stop while user is typing.
*/
QString File::readLine(bool delEOL)
{
  TRACE;
  int bufLen=256;
  char * buf=new char[bufLen];
  CoreApplication::instance()->debugUserInterrupts(false);
  File::readLine(buf, bufLen, stdin, delEOL);
  CoreApplication::instance()->debugUserInterrupts(true);
  QString line(buf);
  delete [] buf;
  return line;
}

/*!
  Overload function for convenience. Returns the line read from compressed (gzip) file \a f.
*/
bool File::readLine(char *& buf, int& bufLen, gzFile f, bool delEOL)
{
  TRACE;
  buf[0]='\0';
  if(!gzgets (f, buf, bufLen)) return false;
  while((int)strlen(buf)==bufLen-1) {
    int nbufLen=bufLen << 1;
    char * nbuf=new char [nbufLen];
    memmove(nbuf,buf,bufLen-1);
    delete [] buf;
    buf=nbuf;
    gzgets (f, buf+bufLen-1, bufLen+1);
    bufLen=nbufLen;
  }
  if(delEOL) {
    int n=strlen(buf)-1;
    if(buf[n]=='\n') {
      if(buf[n-1]=='\r') {
        buf[n-1]='\0';
      } else {
        buf[n]='\0';
      }
    }
  }
  return true;
}

/*!
  Returns the line read from file \a f. If the line ends with \\ the next lines are concatenated
  until a line without \\ at the end.
*/
void File::readBreakLine(char *& buf, int& bufLen, FILE * f)
{
  TRACE;
  buf[0]='\0';
  if(!fgets(buf, bufLen, f)) {
    return;
  }
  int n=strlen(buf);
  while(true) {
    while(n==bufLen-1 && buf[n-1]!='\n') {
      int nbufLen=bufLen << 1;
      char * nbuf=new char [nbufLen];
      memmove(nbuf,buf,n);
      delete [] buf;
      buf=nbuf;
      if(!fgets(buf+n, nbufLen-n, f)) {
        return;
      }
      n=strlen(buf);
      bufLen=nbufLen;
    }
    char * ptr=buf+n-1;
    while(ptr>buf && isspace(*ptr)) ptr--;
    if(*ptr!='\\') break;
    n=ptr-buf;
    if(!fgets (buf+n, bufLen-n, f)) {
      return;
    }
    n=strlen(buf);
    //printf("%i,%i:%s---\n",n,bufLen,buf);
  }
}

/*!
  Delete \a buf and return \a returnValue. Handy for parsers based on readLine().
*/
bool File::readCleanUp(char * buf, bool returnValue)
{
  TRACE;
  delete [] buf;
  return returnValue;
}

/*!
  Read next line on stream \a f stripping out comments (lines starting with '#').
  Comments are eventually stored to \a comments if it is not null.
*/
void File::readLineNoComments(char * buf, int& bufLen, FILE * f, QString * comments)
{
  TRACE;
  while(true) {
    readLine(buf, bufLen, f, true);
    if(buf[0]=='#') {
      if(comments) {
        comments->append(buf);
        comments->append("\n");
      }
    } else break;
  };
}
/*!
  Read next line on stream \a s stripping out comments (lines starting with '#').
  Comments are eventually stored to \a comments if it is not null.
  If \a lineNumber is not null, it is incremented after reading each line.
*/
QString File::readLineNoComments(QTextStream& s, QString * comments, int * lineNumber)
{
  TRACE;
  QString buf;
  while(true) {
    buf=s.readLine();
    if(lineNumber) lineNumber++;
    if(buf[0]=='#') {
      if(comments) {
        comments->append(buf);
        comments->append("\n");
      }
    } else break;
  };
  return buf;
}

/*!
  Extract \a value from any file like a Makefile or a Qt .pro file, i.e. made of format: KEYWORD=VALUE

  Lines can broken by \\.
*/
void File::getKeyValue (const char * fileName, const char * varName, QString& value)
{
  TRACE;
  FILE * f=fopen(fileName,"r");
  if(!f) return;
  int varNameLen=strlen(varName);
  int buflen=512;
  char * buf=new char [buflen];
  while(!feof(f)) {
    readBreakLine(buf,buflen,f);
    char * ptr=stripWhiteSpace(buf);
    if(strncmp(varName,ptr,varNameLen)==0) {
      ptr+=varNameLen;
      ptr=stripWhiteSpace(ptr);
      if(*ptr=='=') {
        ptr++;
        ptr=stripWhiteSpace(ptr);
        value=ptr;
      } else if(*ptr=='+' && *(ptr+1)=='=') {
        ptr+=2;
        ptr=stripWhiteSpace(ptr);
        value+=ptr;
      }
    }
  }
  if(value[0]=='\"' &&
      value[value.length()-1]=='\"') {
    value=value.mid(1,value.length()-2);
  }
  delete [] buf;
  fclose(f);
}

/*!
  Return a pointer to a string without initial and end blanks (isspace()).
  No new buffer is allocated. The intput buffer \a buf is eventually modified to add
  a '\\0' to end the string before the last blanks. The returned pointer cannot be
  deleted with free or delete.
*/
char * File::stripWhiteSpace(char * buf)
{
  TRACE;
  if(!buf) return 0;
  char * ptrE=buf+strlen(buf);
  // Remove white space at the end and at the beginning
  while(buf<ptrE && isspace(*buf)) buf++;
  ptrE--;
  while(buf<ptrE && isspace(*ptrE)) ptrE--;
  if(buf<ptrE) {
    ptrE++;
    *ptrE='\0';
  }
  return buf;
}

/*!
  Overload function for convenience. By contrast with stripWhiteSpace(char * buf) the
  input buffer is not touched but \a len is eventually decreased.
*/
const char * File::stripWhiteSpace(const char * buf, int& len)
{
  TRACE;
  if(!buf) return 0;
  const char * ptrE=buf+len;
  // Remove white space at the end and at the beginning
  while(buf<ptrE && isspace(*buf)) buf++;
  ptrE--;
  while(buf<ptrE && isspace(*ptrE)) ptrE--;
  if(buf<ptrE) len=ptrE-buf+1; else len=0;
  return buf;
}

/*!
  Split \a buf into fields separated with one of more separators \a sep. Return 0 if no more fields are found.
*/
char * File::nextField(char *& buf, const char * sep)
{
  TRACE;
  while( *buf!=0 && strchr(sep, *buf)!=NULL) {
    buf++;
  }
  if( *buf!=0) {
    char * ptr=buf;
    while( *buf!=0 && strchr(sep, *buf)==NULL) {
      buf++;
    }
    if( *buf!=0) {
      *buf=0;
      buf++;
    }
    return ptr;
  } else return 0;
}

/*!
  Return the list of all dynamic libraries available in pathList
*/
QStringList File::getLibList(QStringList pathList)
{
  TRACE;
  QStringList libFilter;
#if defined(Q_OS_MAC)
  libFilter << "*.dylib";
#elif defined(Q_OS_WIN)
  libFilter << "*.dll";
#else
  libFilter << "lib*.so";
#endif
  QStringList absLibs;
  for(QStringList::iterator itPath=pathList.begin(); itPath!=pathList.end(); ++itPath) {
    QDir d(*itPath);
    QStringList libs=d.entryList(libFilter);
    for(QStringList::iterator itLib=libs.begin(); itLib!=libs.end(); ++itLib) {
	  QFileInfo fi(d.absoluteFilePath(*itLib));
	  if(!fi.isSymLink()) {
        absLibs << fi.absoluteFilePath();
      }
    }
  }
  return absLibs;
}

/*!
  Swap the number \a v only if the current platform is not Big Endian
*/
qint32 File::fromBigEndian(qint32 v)
{
  TRACE;
#if Q_BYTE_ORDER==Q_LITTLE_ENDIAN
  union Swaper {
    qint32 i;
    struct {
      char b[4];
    };
  };
  Swaper s1, s2;
  s1.i=v;
  s2.b[0]=s1.b[3];
  s2.b[1]=s1.b[2];
  s2.b[2]=s1.b[1];
  s2.b[3]=s1.b[0];
  return s2.i;
#else
  return v;
#endif
}

/*!
  Swap the number \a v only if the current platform is not Lttle Endian
*/
qint32 File::fromLittleEndian(qint32 v)
{
  TRACE;
#if Q_BYTE_ORDER==Q_BIG_ENDIAN
  union Swaper {
    qint32 i;
    struct {
      char b[4];
    };
  };
  Swaper s1, s2;
  s1.i=v;
  s2.b[0]=s1.b[3];
  s2.b[1]=s1.b[2];
  s2.b[2]=s1.b[1];
  s2.b[3]=s1.b[0];
  return s2.i;
#else
  return v;
#endif
}

/*!
  Swap the number \a v only if the current platform is not Big Endian
*/
qint16 File::fromBigEndian(qint16 v)
{
  TRACE;
#if Q_BYTE_ORDER==Q_LITTLE_ENDIAN
  union Swaper {
    qint16 i;
    struct {
      char b[2];
    };
  };
  Swaper s1, s2;
  s1.i=v;
  s2.b[0]=s1.b[1];
  s2.b[1]=s1.b[0];
  return s2.i;
#else
  return v;
#endif
}

/*!
  Swap the number \a v only if the current platform is not Lttle Endian
*/
qint16 File::fromLittleEndian(qint16 v)
{
  TRACE;
#if Q_BYTE_ORDER==Q_BIG_ENDIAN
  union Swaper {
    qint16 i;
    struct {
      char b[2];
    };
  };
  Swaper s1, s2;
  s1.i=v;
  s2.b[0]=s1.b[1];
  s2.b[1]=s1.b[0];
  return s2.i;
#else
  return v;
#endif
}

void File::readUntilPattern(QTextStream& s, const QString& pattern)
{
  TRACE;
  if(!pattern.isEmpty()) {
    QString line;
    qint64 pos=s.pos();
    while(!s.atEnd()) {
      line=s.readLine();
      if(line.contains(pattern)) {
        pos=-1;
        break;
      }
    }
    if(pos>=0) { // Found no pattern, ignore it and read everything from the beginning
      s.seek(pos);
    }
  }
}

} // namespace QGpCoreTools
