#include "graphhelper.h"

#include <QDebug>
#include <QFileInfo>

#include <utils/processutils.h>

using namespace vnotex;

#define TaskIdProperty "GraphTaskId"
#define TaskTimeStampProperty "GraphTaskTimeStamp"

GraphHelper::GraphHelper() : m_cache(100, CacheItem()) {}

QStringList GraphHelper::getArgsToUse(const QStringList &p_args) {
  if (p_args.isEmpty()) {
    return QStringList();
  }

  if (p_args[0] == "-c") {
    // Combine all the arguments except the first one.
    QStringList args;
    args << p_args[0];

    QString subCmd;
    for (int i = 1; i < p_args.size(); ++i) {
      subCmd += " " + p_args[i];
    }
    args << subCmd;

    return args;
  } else {
    return p_args;
  }
}

void GraphHelper::process(quint64 p_id, TimeStamp p_timeStamp, const QString &p_format,
                          const QString &p_text, QObject *p_owner,
                          const ResultCallback &p_callback) {
  Task task;
  task.m_id = p_id;
  task.m_timeStamp = p_timeStamp;
  task.m_format = p_format;
  task.m_text = p_text;
  task.m_owner = p_owner;
  task.m_callback = p_callback;

  m_tasks.enqueue(task);

  processOneTask();
}

void GraphHelper::processOneTask() {
  if (m_taskOngoing || m_tasks.isEmpty()) {
    return;
  }

  m_taskOngoing = true;

  const auto &task = m_tasks.head();

  const auto &cachedData = m_cache.get(task.m_text);
  if (!cachedData.isNull() && cachedData.m_format == task.m_format) {
    finishOneTask(cachedData.m_data);
    return;
  }

  if (!m_programValid) {
    qWarning() << "program to execute for rendering is not valid" << m_program;
    finishOneTask(QString());
    return;
  }

  // Will be released in finishOneTask.
  QProcess *process = new QProcess();
  process->setProperty(TaskIdProperty, task.m_id);
  process->setProperty(TaskTimeStampProperty, task.m_timeStamp);
  QObject::connect(process, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
                   [this, process](int exitCode, QProcess::ExitStatus exitStatus) {
                     finishOneTask(process, exitCode, exitStatus);
                   });

  if (m_overriddenCommand.isEmpty()) {
    Q_ASSERT(!m_program.isEmpty());
    QStringList args(m_args);
    args << getFormatArgs(task.m_format);
    process->start(m_program, getArgsToUse(args));
  } else {
    auto cmd = getCommandToUse(m_overriddenCommand, task.m_format);
    process->start(cmd);
  }

  if (process->write(task.m_text.toUtf8()) == -1) {
    qWarning() << "Graph task" << task.m_id
               << "failed to write to process stdin:" << process->errorString();
  }

  process->closeWriteChannel();
}

void GraphHelper::finishOneTask(QProcess *p_process, int p_exitCode,
                                QProcess::ExitStatus p_exitStatus) {
  Q_ASSERT(m_taskOngoing && !m_tasks.isEmpty());

  const auto task = m_tasks.dequeue();

  const quint64 id = p_process->property(TaskIdProperty).toULongLong();
  const quint64 timeStamp = p_process->property(TaskTimeStampProperty).toULongLong();
  Q_ASSERT(task.m_id == id && task.m_timeStamp == timeStamp);

  qDebug() << "Graph task" << id << timeStamp << "finished";

  bool failed = true;
  if (p_exitStatus == QProcess::NormalExit) {
    if (p_exitCode < 0) {
      qWarning() << "Graph task" << id << "failed:" << p_exitCode;
    } else {
      failed = false;
      const auto outBa = p_process->readAllStandardOutput();
      QString data;
      if (task.m_format == QStringLiteral("svg")) {
        data = QString::fromUtf8(outBa);
        callbackOneTask(task, id, timeStamp, task.m_format, data);
      } else {
        data = QString::fromLocal8Bit(outBa.toBase64());
        callbackOneTask(task, id, timeStamp, task.m_format, data);
      }

      CacheItem item;
      item.m_format = task.m_format;
      item.m_data = data;
      m_cache.set(task.m_text, item);
    }
  } else {
    qWarning() << "Graph task" << id << "failed to start" << p_exitCode << p_exitStatus;
  }

  const QByteArray errBa = p_process->readAllStandardError();
  if (!errBa.isEmpty()) {
    QString errStr(QString::fromUtf8(errBa));
    if (failed) {
      qWarning() << "Graph task" << id << "stderr:" << errStr;
    } else {
      qDebug() << "Graph task" << id << "stderr:" << errStr;
    }
  }

  if (failed) {
    callbackOneTask(task, id, task.m_timeStamp, task.m_format, QString());
  }

  p_process->deleteLater();

  m_taskOngoing = false;
  processOneTask();
}

void GraphHelper::finishOneTask(const QString &p_data) {
  Q_ASSERT(m_taskOngoing && !m_tasks.isEmpty());

  const auto task = m_tasks.dequeue();

  qDebug() << "Graph task" << task.m_id << task.m_timeStamp << "finished by cache" << p_data.size();

  callbackOneTask(task, task.m_id, task.m_timeStamp, task.m_format, p_data);

  m_taskOngoing = false;
  processOneTask();
}

QString GraphHelper::getCommandToUse(const QString &p_command, const QString &p_format) {
  auto cmd(p_command);
  cmd.replace("%1", p_format);
  return cmd;
}

void GraphHelper::clearCache() { m_cache.clear(); }

void GraphHelper::checkValidProgram() {
  m_programValid = true;
  if (m_overriddenCommand.isEmpty()) {
    if (m_program.isEmpty()) {
      m_programValid = false;
    } else {
      QFileInfo finfo(m_program);
      m_programValid = !finfo.isAbsolute() || finfo.isExecutable();
    }
  }
}

void GraphHelper::callbackOneTask(const Task &p_task, quint64 p_id, TimeStamp p_timeStamp,
                                  const QString &p_format, const QString &p_data) const {
  if (p_task.m_owner) {
    p_task.m_callback(p_id, p_timeStamp, p_format, p_data);
  }
}
