text: //
  //  Created by Ryan Huffman on 05/12/14.
  //  Copyright 2014 High Fidelity, Inc.
  //
  //  S3 request code written with ModelBrowser as a reference.
  //
  //  Distributed under the Apache License, Version 2.0.
  //  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
  //
  
  #include "ScriptsModel.h"
  
  #include <QUrl>
  #include <QUrlQuery>
  #include <QXmlStreamReader>
  #include <QDirIterator>
  
  #include <NetworkAccessManager.h>
  #include <PathUtils.h>
  
  #include "ScriptEngine.h"
  #include "ScriptEngines.h"
  #include "ScriptEngineLogging.h"
  #define __STR2__(x) #x
  #define __STR1__(x) __STR2__(x)
  #define __LOC__ __FILE__ "(" __STR1__(__LINE__) ") : Warning Msg: "
  
  static const QString PREFIX_PARAMETER_NAME = "prefix";
  static const QString MARKER_PARAMETER_NAME = "marker";
  static const QString IS_TRUNCATED_NAME = "IsTruncated";
  static const QString CONTAINER_NAME = "Contents";
  static const QString KEY_NAME = "Key";
  
  TreeNodeBase::TreeNodeBase(TreeNodeFolder* parent, const QString& name, TreeNodeType type) :
      _parent(parent),
      _type(type),
      _name(name) {
  };
  
  TreeNodeScript::TreeNodeScript(const QString& localPath, const QString& fullPath, ScriptOrigin origin) :
      TreeNodeBase(NULL, localPath.split("/").last(), TREE_NODE_TYPE_SCRIPT),
      _localPath(localPath),
      _fullPath(expandScriptUrl(QUrl(fullPath)).toString()),
      _origin(origin) {
  };
  
  TreeNodeFolder::TreeNodeFolder(const QString& foldername, TreeNodeFolder* parent) :
      TreeNodeBase(parent, foldername, TREE_NODE_TYPE_FOLDER) {
  };
  
  ScriptsModel::ScriptsModel(QObject* parent) :
      QAbstractItemModel(parent),
      _loadingScripts(false),
      _localDirectory(),
      _fsWatcher(),
      _treeNodes()
  {
      _localDirectory.setFilter(QDir::Files | QDir::Readable);
      _localDirectory.setNameFilters(QStringList("*.js"));
  
      connect(&_fsWatcher, &QFileSystemWatcher::directoryChanged, this, &ScriptsModel::reloadLocalFiles);
      reloadLocalFiles();
      reloadDefaultFiles();
  }
  
  ScriptsModel::~ScriptsModel() {
      for (int i = _treeNodes.size() - 1; i >= 0; i--) {
          delete _treeNodes.at(i);
      }
      _treeNodes.clear();
  }
  
  TreeNodeBase* ScriptsModel::getTreeNodeFromIndex(const QModelIndex& index) const {
      if (index.isValid()) {
          return static_cast<TreeNodeBase*>(index.internalPointer());
      }
      return NULL;
  }
  
  QModelIndex ScriptsModel::index(int row, int column, const QModelIndex& parent) const {
      if (row < 0 || column < 0) {
          return QModelIndex();
      }
      return createIndex(row, column, getFolderNodes(static_cast<TreeNodeFolder*>(getTreeNodeFromIndex(parent))).at(row));
  }
  
  QModelIndex ScriptsModel::parent(const QModelIndex& child) const {
      TreeNodeFolder* parent = (static_cast<TreeNodeBase*>(child.internalPointer()))->getParent();
      if (!parent) {
          return QModelIndex();
      }
      TreeNodeFolder* grandParent = parent->getParent();
      int row = getFolderNodes(grandParent).indexOf(parent);
      return createIndex(row, 0, parent);
  }
  
  QVariant ScriptsModel::data(const QModelIndex& index, int role) const {
      TreeNodeBase* node = getTreeNodeFromIndex(index);
      if (!node) {
          return QVariant();
      }
      if (node->getType() == TREE_NODE_TYPE_SCRIPT) {
          TreeNodeScript* script = static_cast<TreeNodeScript*>(node);
          if (role == Qt::DisplayRole) {
              return QVariant(script->getName() + (script->getOrigin() == SCRIPT_ORIGIN_LOCAL ? " (local)" : ""));
          } else if (role == ScriptPath) {
              return QVariant(script->getFullPath());
          }
      } else if (node->getType() == TREE_NODE_TYPE_FOLDER) {
          TreeNodeFolder* folder = static_cast<TreeNodeFolder*>(node);
          if (role == Qt::DisplayRole) {
              return QVariant(folder->getName());
          }
      }
      return QVariant();
  }
  
  int ScriptsModel::rowCount(const QModelIndex& parent) const {
      return getFolderNodes(static_cast<TreeNodeFolder*>(getTreeNodeFromIndex(parent))).count();
  }
  
  int ScriptsModel::columnCount(const QModelIndex& parent) const {
      return 1;
  }
  
  void ScriptsModel::updateScriptsLocation(const QString& newPath) {
      _fsWatcher.removePath(_localDirectory.absolutePath());
      
      if (!newPath.isEmpty()) {
          _localDirectory.setPath(newPath);
          
          if (!_localDirectory.absolutePath().isEmpty()) {
              _fsWatcher.addPath(_localDirectory.absolutePath());
          }
      }
      
      reloadLocalFiles();
  }
  
  void ScriptsModel::reloadDefaultFiles() {
      if (!_loadingScripts) {
          _loadingScripts = true;
          for (int i = _treeNodes.size() - 1; i >= 0; i--) {
              TreeNodeBase* node = _treeNodes.at(i);
              if (node->getType() == TREE_NODE_TYPE_SCRIPT &&
                  static_cast<TreeNodeScript*>(node)->getOrigin() == SCRIPT_ORIGIN_DEFAULT)
              {
                  delete node;
                  _treeNodes.removeAt(i);
              }
          }
          requestDefaultFiles();
      }
  }
  
  void ScriptsModel::requestDefaultFiles(QString marker) {
      QUrl url(defaultScriptsLocation());
  
      if (url.isLocalFile()) {
          // if the url indicates a local directory, use QDirIterator
          QString localDir = expandScriptUrl(url).toLocalFile();
          int localDirPartCount = localDir.split("/").size();
          if (localDir.endsWith("/")) {
              localDirPartCount--;
          }
          #ifdef Q_OS_WIN
          localDirPartCount++; // one for the drive letter
          #endif
          QDirIterator it(localDir, QStringList() << "*.js", QDir::Files, QDirIterator::Subdirectories);
          while (it.hasNext()) {
              QUrl jsFullPath = QUrl::fromLocalFile(it.next());
              QString jsPartialPath = jsFullPath.path().split("/").mid(localDirPartCount).join("/");
              jsFullPath = normalizeScriptURL(jsFullPath);
              _treeNodes.append(new TreeNodeScript(jsPartialPath, jsFullPath.toString(), SCRIPT_ORIGIN_DEFAULT));
          }
          _loadingScripts = false;
      } else {
          // the url indicates http(s), use QNetworkRequest
          QUrlQuery query;
          query.addQueryItem(PREFIX_PARAMETER_NAME, ".");
          if (!marker.isEmpty()) {
              query.addQueryItem(MARKER_PARAMETER_NAME, marker);
          }
          url.setQuery(query);
  
          QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance();
          QNetworkRequest request(url);
          request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
          request.setHeader(QNetworkRequest::UserAgentHeader, HIGH_FIDELITY_USER_AGENT);
          QNetworkReply* reply = networkAccessManager.get(request);
          connect(reply, SIGNAL(finished()), SLOT(downloadFinished()));
      }
  }
  
  void ScriptsModel::downloadFinished() {
      QNetworkReply* reply = static_cast<QNetworkReply*>(sender());
      bool finished = true;
  
      if (reply->error() == QNetworkReply::NoError) {
          QByteArray data = reply->readAll();
  
          if (!data.isEmpty()) {
              finished = parseXML(data);
          } else {
              qCDebug(scriptengine) << "Error: Received no data when loading default scripts";
          }
      } else {
          qCDebug(scriptengine) << "Error: when loading default scripts --" << reply->error();
      }
  
      reply->deleteLater();
      sender()->deleteLater();
  
      if (finished) {
          _loadingScripts = false;
      }
  }
  
  bool ScriptsModel::parseXML(QByteArray xmlFile) {
      beginResetModel();
  
      QXmlStreamReader xml(xmlFile);
      QRegExp jsRegex(".*\\.js");
      bool truncated = false;
      QString lastKey;
  
      while (!xml.atEnd() && !xml.hasError()) {
          if (xml.tokenType() == QXmlStreamReader::StartElement && xml.name() == IS_TRUNCATED_NAME) {
              while (!(xml.tokenType() == QXmlStreamReader::EndElement && xml.name() == IS_TRUNCATED_NAME)) {
                  xml.readNext();
                  if (xml.text().toString() == "true") {
                      truncated = true;
                  }
              }
          }
  
          if (xml.tokenType() == QXmlStreamReader::StartElement && xml.name() == CONTAINER_NAME) {
              while (!(xml.tokenType() == QXmlStreamReader::EndElement && xml.name() == CONTAINER_NAME)) {
                  if (xml.tokenType() == QXmlStreamReader::StartElement && xml.name() == KEY_NAME) {
                      xml.readNext();
                      lastKey = xml.text().toString();
                      if (jsRegex.exactMatch(xml.text().toString())) {
                          QString localPath = lastKey.split("/").mid(1).join("/");
                          QUrl fullPath = defaultScriptsLocation();
                          fullPath.setPath(fullPath.path() + lastKey);
                          const QString fullPathStr = normalizeScriptURL(fullPath).toString();
                          _treeNodes.append(new TreeNodeScript(localPath, fullPathStr, SCRIPT_ORIGIN_DEFAULT));
                      }
                  }
                  xml.readNext();
              }
          }
          xml.readNext();
      }
      rebuildTree();
      endResetModel();
  
      // Error handling
      if (xml.hasError()) {
          qCDebug(scriptengine) << "Error loading default scripts: " << xml.errorString();
          return true;
      }
  
      if (truncated) {
          requestDefaultFiles(lastKey);
      }
  
      // If this request was not truncated, we are done.
      return !truncated;
  }
  
  void ScriptsModel::reloadLocalFiles() {
      beginResetModel();
  
      for (int i = _treeNodes.size() - 1; i >= 0; i--) {
          TreeNodeBase* node = _treeNodes.at(i);
          if (node->getType() == TREE_NODE_TYPE_SCRIPT &&
              static_cast<TreeNodeScript*>(node)->getOrigin() == SCRIPT_ORIGIN_LOCAL)
          {
              delete node;
              _treeNodes.removeAt(i);
          }
      }
  
      _localDirectory.refresh();
  
      const QFileInfoList localFiles = _localDirectory.entryInfoList();
      for (int i = 0; i < localFiles.size(); i++) {
          QFileInfo file = localFiles[i];
          QString fileName = file.fileName();
          QUrl absPath = normalizeScriptURL(QUrl::fromLocalFile(file.absoluteFilePath()));
          _treeNodes.append(new TreeNodeScript(fileName, absPath.toString(), SCRIPT_ORIGIN_LOCAL));
      }
      rebuildTree();
      endResetModel();
  }
  
  void ScriptsModel::rebuildTree() {
      for (int i = _treeNodes.size() - 1; i >= 0; i--) {
          if (_treeNodes.at(i)->getType() == TREE_NODE_TYPE_FOLDER) {
              delete _treeNodes.at(i);
              _treeNodes.removeAt(i);
          }
      }
      QHash<QString, TreeNodeFolder*> folders;
      for (int i = 0; i < _treeNodes.size(); i++) {
          TreeNodeBase* node = _treeNodes.at(i);
          if (node->getType() == TREE_NODE_TYPE_SCRIPT) {
              TreeNodeScript* script = static_cast<TreeNodeScript*>(node);
              TreeNodeFolder* parent = NULL;
              QString hash;
              QStringList pathList = script->getLocalPath().split(tr("/"));
              pathList.removeLast();
              if (pathList.isEmpty()) {
                  continue;
              }
              QStringList::const_iterator pathIterator;
              for (pathIterator = pathList.constBegin(); pathIterator != pathList.constEnd(); ++pathIterator) {
                  hash.append(*pathIterator + "/");
                  if (!folders.contains(hash)) {
                      folders[hash] = new TreeNodeFolder(*pathIterator, parent);
                  }
                  parent = folders[hash];
              }
              script->setParent(parent);
          }
      }
      QHash<QString, TreeNodeFolder*>::const_iterator folderIterator;
      for (folderIterator = folders.constBegin(); folderIterator != folders.constEnd(); ++folderIterator) {
          _treeNodes.append(*folderIterator);
      }
      folders.clear();
  }
  
  QList<TreeNodeBase*> ScriptsModel::getFolderNodes(TreeNodeFolder* parent) const {
      QList<TreeNodeBase*> result;
      for (int i = 0; i < _treeNodes.size(); i++) {
          TreeNodeBase* node = _treeNodes.at(i);
          if (node->getParent() == parent) {
              result.append(node);
          }
      }
      return result;
  }
  