/*==============================================================================

  Program: 3D Slicer

  Copyright (c) Kitware Inc.

  See COPYRIGHT.txt
  or http://www.slicer.org/copyright/copyright.txt for details.

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.

  This file was originally developed by Jean-Christophe Fillion-Robin, Kitware Inc.
  and was partially funded by NIH grant 3P41RR013218-12S1

==============================================================================*/

// Qt includes
#include <QDebug>
#include <QDir>
#include <QFileInfo>
#include <QRegularExpression>
#include <QStringList>
#include <QUrl>

// Slicer includes
#include "qSlicerUtils.h"
#include "qSlicerAbstractCoreModule.h"
#include "vtkSlicerConfigure.h"

// SlicerLogic includes
#include "vtkSlicerApplicationLogic.h"

//------------------------------------------------------------------------------
bool qSlicerUtils::isExecutableName(const QString& name)
{
  QStringList extensions;
  extensions << ".bat" << ".com" << ".sh" << ".csh" << ".tcsh"
             << ".pl" << ".py" << ".tcl" << ".m" << ".exe";

  for (const QString& extension : extensions)
  {
    if (name.endsWith(extension, Qt::CaseInsensitive))
    {
      return true;
    }
  }
  return false;
}

//------------------------------------------------------------------------------
bool qSlicerUtils::isCLIExecutable(const QString& filePath)
{
  if (isCLIScriptedExecutable(filePath))
  {
    return true;
  }

#ifdef _WIN32
  return (filePath.endsWith(".exe", Qt::CaseInsensitive) //
          || filePath.endsWith(".bat", Qt::CaseInsensitive));
#else
  return !QFileInfo(filePath).fileName().contains('.');
#endif
}

//------------------------------------------------------------------------------
bool qSlicerUtils::isCLIScriptedExecutable(const QString& filePath)
{
  // check if .py file starts with `#!` magic
  //   note: uses QTextStream to avoid issues with BOM.
  QFile scriptFile(filePath);
  QTextStream scriptStream(&scriptFile);

  if (filePath.endsWith(".py", Qt::CaseInsensitive) //
      && scriptFile.open(QIODevice::ReadOnly)       //
      && scriptStream.readLine(2).startsWith("#!"))
  {
    return true;
  }
  return false;
}

//-----------------------------------------------------------------------------
bool qSlicerUtils::isCLILoadableModule(const QString& filePath)
{
  // See https://stackoverflow.com/questions/899422/regular-expression-for-a-string-that-does-not-start-with-a-sequence
  QRegularExpression regex("^((lib.+Lib\\.(so|dylib))|((?!lib).+Lib\\.(dll|DLL)))$");
  return regex.match(QFileInfo(filePath).fileName()).hasMatch();
}

//-----------------------------------------------------------------------------
bool qSlicerUtils::isLoadableModule(const QString& filePath)
{
  // See https://stackoverflow.com/questions/899422/regular-expression-for-a-string-that-does-not-start-with-a-sequence
  QRegularExpression regex("^((libqSlicer.+Module\\.(so|dylib))|((?!lib)qSlicer.+Module\\.(dll|DLL)))$");
  return regex.match(QFileInfo(filePath).fileName()).hasMatch();
}

//-----------------------------------------------------------------------------
bool qSlicerUtils::isTestingModule(qSlicerAbstractCoreModule* module)
{
  const QStringList& categories = module->categories();
  for (const QString& category : categories)
  {
    if (category.split('.').takeFirst() != "Testing")
    {
      return false;
    }
  }
  return true;
}

//------------------------------------------------------------------------------
QString qSlicerUtils::searchTargetInIntDir(const QString& directory, const QString& target)
{
#ifdef _WIN32
  QStringList intDirs;
  intDirs << "." << "Debug" << "RelWithDebInfo" << "Release" << "MinSizeRel";
  QString intDir = directory + "/%2/" + target;
  for (const QString& subdir : intDirs)
  {
    if (QFile::exists(intDir.arg(subdir)))
    {
      return directory + "/" + subdir + "/";
    }
  }
  return QString();
#else
  Q_UNUSED(target);
  return directory;
#endif
}

//------------------------------------------------------------------------------
QString qSlicerUtils::executableExtension()
{
#ifdef _WIN32
  return QLatin1String(".exe");
#else
  return QString();
#endif
}

//-----------------------------------------------------------------------------
QString qSlicerUtils::extractModuleNameFromLibraryName(const QString& libraryName)
{
  QFileInfo libraryPath(libraryName);
  QString moduleName = libraryPath.baseName();

  // Remove prefix 'lib' if needed
  if (moduleName.indexOf("lib") == 0)
  {
    moduleName.remove(0, 3);
  }

  // Remove prefix 'qSlicer' if needed
  if (moduleName.indexOf("qSlicer") == 0)
  {
    moduleName.remove(0, 7);
  }

  // Remove suffix 'Module' if needed
  int index = moduleName.lastIndexOf("Module");
  if (index != -1)
  {
    moduleName.remove(index, 6);
  }

  // Remove suffix 'Lib' if needed
  index = moduleName.lastIndexOf("Lib");
  if (index != -1 && index == (moduleName.size() - 3))
  {
    moduleName.remove(index, 3);
  }

  return moduleName;
}

//-----------------------------------------------------------------------------
QString qSlicerUtils::extractModuleNameFromClassName(const QString& className)
{
  QString moduleName(className);

  // Remove prefix 'qSlicer' if needed
  if (moduleName.indexOf("qSlicer") == 0)
  {
    moduleName.remove(0, 7);
  }

  // Remove suffix 'Module' if needed
  int index = moduleName.lastIndexOf("Module");
  if (index == (moduleName.size() - 6))
  {
    moduleName.remove(index, 6);
  }

  return moduleName;
}

//-----------------------------------------------------------------------------
bool qSlicerUtils::isPluginInstalled(const QString& filePath, const QString& applicationHomeDir)
{
#ifdef Slicer_BUILD_EXTENSIONMANAGER_SUPPORT
  std::string organizationDomain = Slicer_ORGANIZATION_DOMAIN;
  std::string organizationName = Slicer_ORGANIZATION_NAME;
  std::string extensionsDirBase = Slicer_EXTENSIONS_DIRBASENAME;
#else
  std::string organizationDomain;
  std::string organizationName;
  std::string extensionsDirBase;
#endif

  return vtkSlicerApplicationLogic::IsPluginInstalled(filePath.toStdString(), applicationHomeDir.toStdString(), organizationDomain, organizationName, extensionsDirBase);
}

//-----------------------------------------------------------------------------
bool qSlicerUtils::isPluginBuiltIn(const QString& filePath, const QString& applicationHomeDir, const QString& applicationRevision)
{
#ifdef Slicer_BUILD_EXTENSIONMANAGER_SUPPORT
  std::string extensionsDirBase = Slicer_EXTENSIONS_DIRBASENAME;
#else
  std::string extensionsDirBase;
#endif

  return vtkSlicerApplicationLogic::IsPluginBuiltIn(filePath.toStdString(), applicationHomeDir.toStdString(), applicationRevision.toStdString(), extensionsDirBase);
}

//-----------------------------------------------------------------------------
QString qSlicerUtils::pathWithoutIntDir(const QString& path, const QString& subDirWithoutIntDir)
{
  QString tmp;
  return qSlicerUtils::pathWithoutIntDir(path, subDirWithoutIntDir, tmp);
}

//-----------------------------------------------------------------------------
QString qSlicerUtils::pathWithoutIntDir(const QString& path, const QString& subDirWithoutIntDir, QString& intDir)
{
  QDir pathAsDir(path);
  if (!qSlicerUtils::pathEndsWith(path, subDirWithoutIntDir))
  {
    intDir = pathAsDir.dirName();
    pathAsDir.cdUp();
    if (!qSlicerUtils::pathEndsWith(pathAsDir.path(), subDirWithoutIntDir))
    {
      intDir.clear();
      return path;
    }
  }
  return pathAsDir.path();
}

//-----------------------------------------------------------------------------
bool qSlicerUtils::pathEndsWith(const QString& inputPath, const QString& path)
{
#ifdef Q_OS_WIN32
  Qt::CaseSensitivity sensitivity = Qt::CaseInsensitive;
#else
  Qt::CaseSensitivity sensitivity = Qt::CaseSensitive;
#endif
  return QDir::cleanPath(QDir::fromNativeSeparators(inputPath)).endsWith(QDir::cleanPath(QDir::fromNativeSeparators(path)), sensitivity);
}

//-----------------------------------------------------------------------------
bool qSlicerUtils::setPermissionsRecursively(const QString& path, QFile::Permissions directoryPermissions, QFile::Permissions filePermissions)
{
  if (!QFile::exists(path))
  {
    qCritical() << "qSlicerUtils::setPermissionsRecursively: Failed to set permissions of nonexistent file" << path;
    return false;
  }

  for (const QFileInfo& info : QDir(path).entryInfoList(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot))
  {
    if (info.isDir())
    {
      if (directoryPermissions & QFile::ExeOwner    //
          || directoryPermissions & QFile::ExeGroup //
          || directoryPermissions & QFile::ExeOther)
      {
        // If executable bit is on /a/b/c/d, we should start with a, b, c, then d
        if (!QFile::setPermissions(info.filePath(), directoryPermissions))
        {
          qCritical() << "qSlicerUtils::setPermissionsRecursively: Failed to set permissions on directory" << info.filePath();
          return false;
        }
        if (!qSlicerUtils::setPermissionsRecursively(info.filePath(), directoryPermissions, filePermissions))
        {
          return false;
        }
      }
      else
      {
        // .. otherwise we should start with d, c, b, then a
        if (!qSlicerUtils::setPermissionsRecursively(info.filePath(), directoryPermissions, filePermissions))
        {
          return false;
        }
        if (!QFile::setPermissions(info.filePath(), directoryPermissions))
        {
          qCritical() << "qSlicerUtils::setPermissionsRecursively: Failed to set permissions on directory" << info.filePath();
          return false;
        }
      }
    }
    else if (info.isFile())
    {
      if (!QFile::setPermissions(info.filePath(), filePermissions))
      {
        qCritical() << "qSlicerUtils::setPermissionsRecursively: Failed to set permissions on file" << info.filePath();
        return false;
      }
    }
    else
    {
      qWarning() << "qSlicerUtils::setPermissionsRecursively: Unhandled item" << info.filePath();
    }
  }
  return true;
}

//-----------------------------------------------------------------------------
QString qSlicerUtils::replaceWikiUrlVersion(const QString& text, const QString& version)
{
  QString updatedText = text;
  QRegularExpression rx("http[s]?\\:\\/\\/[a-zA-Z0-9\\-\\._\\?\\,\\'\\/\\\\\\+&amp;%\\$#\\=~]*");
  QRegularExpressionMatchIterator iter = rx.globalMatch(updatedText);
  int offset = 0;
  while (iter.hasNext())
  {
    QRegularExpressionMatch match = iter.next();
    int pos = match.capturedStart() + offset;
    QString capturedText = match.captured(0);
    // Given an URL matching the regular expression reported above, this second
    // expression will replace the first occurrence of "Documentation/<StringWithLetterOrNumberOrDot>/"
    // with "Documentation/<version>/"
    QString updatedURL = capturedText;
    QRegularExpression docRegex("Documentation\\/[a-zA-Z0-9\\.]+");
    updatedURL.replace(docRegex, "Documentation/" + version);
    if (updatedURL != capturedText)
    {
      updatedText.replace(pos, capturedText.length(), updatedURL);
      offset += updatedURL.length() - capturedText.length();
    }
  }

  return updatedText;
}

bool replaceFirst(QString& text, const QString& pattern, const QString& replacement)
{
  QRegularExpression rx(pattern);
  QRegularExpressionMatch match = rx.match(text);
  if (!match.hasMatch())
  {
    return false;
  }
  text = text.replace(match.capturedStart(), match.capturedLength(), replacement);
  return true;
}

//-----------------------------------------------------------------------------
QString qSlicerUtils::replaceDocumentationUrlVersion(const QString& text, const QString& hostname, const QString& version)
{
  QString updatedText = text;
  QRegularExpression rx("http[s]?\\:\\/\\/[a-zA-Z0-9\\-\\._\\?\\,\\'\\/\\\\\\+&amp;%\\$#\\=~]*");
  QRegularExpressionMatchIterator iter = rx.globalMatch(updatedText);
  int offset = 0;
  while (iter.hasNext())
  {
    // Given an URL matching the regular expression reported above, this second
    // expression will replace the first occurrence of "/<StringWithLetterOrNumberOrDot>/" or "/<StringWithLetterOrNumberOrDot>#"
    // with "/<version>/" or "/<version>#".
    QRegularExpressionMatch match = iter.next();
    int pos = match.capturedStart() + offset;
    QString foundURL = match.captured(0);
    QString originalURL = foundURL;
    if (foundURL.contains(hostname)                                                              //
        && (replaceFirst(foundURL, "\\/[0-9\\.]+\\/|/latest\\/|/stable\\/", "/" + version + "/") // replace /5.0/
            || replaceFirst(foundURL, "\\/v[0-9\\.]+\\/", "/" + version + "/"))                  // replace /v5.0/
    )
    {
      updatedText.replace(pos, originalURL.length(), foundURL);
      offset += foundURL.length() - originalURL.length();
    }
  }

  return updatedText;
}

//-----------------------------------------------------------------------------
QString qSlicerUtils::safeQStringFromUtf8Ptr(const char* cString)
{
  if (!cString)
  {
    return QString();
  }
  return QString::fromUtf8(cString);
}
