/*
 * Copyright (C) 2013 - 2015  Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
 *
 * 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 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
 * 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
 *
 */


#include "pathedit.h"
#include "pathedit_p.h"
#include <QCompleter>
#include <QStringListModel>
#include <QStringBuilder>
#include <QThread>
#include <QDebug>
#include <QKeyEvent>
#include <libfm/fm.h>

namespace Fm {

void PathEditJob::runJob() {
  GError *err = NULL;
  GFileEnumerator* enu = g_file_enumerate_children(dirName,
                                                   // G_FILE_ATTRIBUTE_STANDARD_NAME","
                                                   G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME","
                                                   G_FILE_ATTRIBUTE_STANDARD_TYPE,
                                                   G_FILE_QUERY_INFO_NONE, cancellable,
                                                   &err);
  if(enu) {
    while(!g_cancellable_is_cancelled(cancellable)) {
      GFileInfo* inf = g_file_enumerator_next_file(enu, cancellable, &err);
      if(inf) {
        GFileType type = g_file_info_get_file_type(inf);
        if(type == G_FILE_TYPE_DIRECTORY) {
          const char* name = g_file_info_get_display_name(inf);
          // FIXME: encoding conversion here?
          subDirs.append(QString::fromUtf8(name));
        }
        g_object_unref(inf);
      }
      else {
        if(err) {
          g_error_free(err);
          err = NULL;
        }
        else /* EOF */
          break;
      }
    }
    g_file_enumerator_close(enu, cancellable, NULL);
    g_object_unref(enu);
  }
  // finished! let's update the UI in the main thread
  Q_EMIT finished();
}


PathEdit::PathEdit(QWidget* parent):
  QLineEdit(parent),
  completer_(new QCompleter()),
  model_(new QStringListModel()),
  cancellable_(NULL) {
  setCompleter(completer_);
  completer_->setModel(model_);
  connect(this, &PathEdit::textChanged, this, &PathEdit::onTextChanged);
}

PathEdit::~PathEdit() {
  delete completer_;
  if(model_)
    delete model_;
  if(cancellable_) {
    g_cancellable_cancel(cancellable_);
    g_object_unref(cancellable_);
  }
}

void PathEdit::focusInEvent(QFocusEvent* e) {
  QLineEdit::focusInEvent(e);
  // build the completion list only when we have the keyboard focus
  reloadCompleter(true);
}

void PathEdit::focusOutEvent(QFocusEvent* e) {
  QLineEdit::focusOutEvent(e);
  // free the completion list since we don't need it anymore
  freeCompleter();
}

bool PathEdit::event(QEvent* e) {
  // Stop Qt from moving the keyboard focus to the next widget when "Tab" is pressed.
  // Instead, we need to do auto-completion in this case.
  if(e->type() == QEvent::KeyPress) {
    QKeyEvent* keyEvent = static_cast<QKeyEvent*>(e);
    if(keyEvent->key() == Qt::Key_Tab && keyEvent->modifiers() == Qt::NoModifier) { // Tab key is pressed
      e->accept();
      // do auto-completion when the user press the Tab key.
      // This fixes #201: https://github.com/lxde/pcmanfm-qt/issues/201
      autoComplete();
      return true;
    }
  }
  return QLineEdit::event(e);
}

void PathEdit::onTextChanged(const QString& text) {
  int pos = text.lastIndexOf('/');
  if(pos >= 0)
    ++pos;
  else
    pos = text.length();
  QString newPrefix = text.left(pos);
  if(currentPrefix_ != newPrefix) {
    currentPrefix_ = newPrefix;
    // only build the completion list if we have the keyboard focus
    // if we don't have the focus now, then we'll rebuild the completion list
    // when focusInEvent happens. this avoid unnecessary dir loading.
    if(hasFocus())
      reloadCompleter(false);
  }
}

void PathEdit::autoComplete() {
  // find longest common prefix of the strings currently shown in the candidate list
  QAbstractItemModel* model = completer_->completionModel();
  if(model->rowCount() > 0) {
    int minLen = text().length();
    QString commonPrefix = model->data(model->index(0, 0)).toString();
    for(int row = 1; row < model->rowCount() && commonPrefix.length() > minLen; ++row) {
      QModelIndex index = model->index(row, 0);
      QString rowText = model->data(index).toString();
      int prefixLen = 0;
      while(prefixLen < rowText.length() && prefixLen < commonPrefix.length() && rowText[prefixLen] == commonPrefix[prefixLen]) {
        ++prefixLen;
      }
      commonPrefix.truncate(prefixLen);
    }
    if(commonPrefix.length() > minLen) {
      setText(commonPrefix);
    }
  }
}

void PathEdit::reloadCompleter(bool triggeredByFocusInEvent) {
  // parent dir has been changed, reload dir list
  // if(currentPrefix_[0] == "~") { // special case for home dir
  // cancel running dir-listing jobs, if there's any
  if(cancellable_) {
    g_cancellable_cancel(cancellable_);
    g_object_unref(cancellable_);
  }

  // create a new job to do dir listing
  PathEditJob* job = new PathEditJob();
  job->edit = this;
  job->triggeredByFocusInEvent = triggeredByFocusInEvent;
  // need to use fm_file_new_for_commandline_arg() rather than g_file_new_for_commandline_arg().
  // otherwise, our own vfs, such as menu://, won't be loaded.
  job->dirName = fm_file_new_for_commandline_arg(currentPrefix_.toLocal8Bit().constData());
  // qDebug("load: %s", g_file_get_uri(data->dirName));
  cancellable_ = g_cancellable_new();
  job->cancellable = (GCancellable*)g_object_ref(cancellable_);

  // launch a new worker thread to handle the job
  QThread* thread = new QThread();
  job->moveToThread(thread);
  connect(thread, &QThread::started, job, &PathEditJob::runJob);
  connect(thread, &QThread::finished, thread, &QObject::deleteLater);
  connect(thread, &QThread::finished, job, &QObject::deleteLater);
  connect(job, &PathEditJob::finished, this, &PathEdit::onJobFinished);
  thread->start(QThread::LowPriority);
}

void PathEdit::freeCompleter() {
  if(cancellable_) {
    g_cancellable_cancel(cancellable_);
    g_object_unref(cancellable_);
    cancellable_ = NULL;
  }
  model_->setStringList(QStringList());
}

// This slot is called from main thread so it's safe to access the GUI
void PathEdit::onJobFinished() {
  PathEditJob* data = static_cast<PathEditJob*>(sender());
  if(!g_cancellable_is_cancelled(data->cancellable)) {
    // update the completer only if the job is not cancelled
    QStringList::iterator it;
    for(it = data->subDirs.begin(); it != data->subDirs.end(); ++it) {
      // qDebug("%s", it->toUtf8().constData());
      *it = (currentPrefix_ % *it);
    }
    model_->setStringList(data->subDirs);
    // trigger completion manually
    if(hasFocus() && !data->triggeredByFocusInEvent)
      completer_->complete();
  }
  else
    model_->setStringList(QStringList());
  if(cancellable_) {
    g_object_unref(cancellable_);
    cancellable_ = NULL;
  }
}

} // namespace Fm
