/*
 * Copyright (C) 2012 Open Source Robotics Foundation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * 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.
 *
 */

#ifdef _WIN32
  // For _access()
  #include <io.h>
#endif

#include <boost/filesystem.hpp>
#include <stdio.h>

#include "gazebo/common/CommonIface.hh"
#include "gazebo/common/SystemPaths.hh"
#include "gazebo/gui/DataLogger.hh"
#include "gazebo/gui/DataLoggerPrivate.hh"
#include "gazebo/msgs/msgs.hh"
#include "gazebo/transport/transport.hh"

using namespace gazebo;
using namespace gui;

/////////////////////////////////////////////////
DataLogger::DataLogger(QWidget *_parent)
  : QDialog(_parent),
    dataPtr(new DataLoggerPrivate())
{
  this->setWindowFlags(Qt::Window | Qt::WindowTitleHint |
      Qt::WindowStaysOnTopHint | Qt::CustomizeWindowHint);

  // This name is used in the qt style sheet
  this->setObjectName("dataLogger");
  this->setWindowIcon(QIcon(":/images/gazebo.svg"));
  this->setWindowTitle(tr("Gazebo: Data Logger"));
  this->setWindowFlags(Qt::Window | Qt::WindowCloseButtonHint |
      Qt::WindowStaysOnTopHint | Qt::CustomizeWindowHint);

  // The record button allows the user to start and pause data recording
  this->dataPtr->recordButton = new QToolButton(this);
  this->dataPtr->recordButton->setIcon(QPixmap(":/images/record.png"));
  this->dataPtr->recordButton->setStatusTip(tr("Record a log file"));
  this->dataPtr->recordButton->setCheckable(true);
  this->dataPtr->recordButton->setChecked(false);
  this->dataPtr->recordButton->setIconSize(QSize(30, 30));
  this->dataPtr->recordButton->setObjectName("dataLoggerRecordButton");
  connect(this->dataPtr->recordButton, SIGNAL(toggled(bool)),
          this, SLOT(OnRecord(bool)));

  // Textual status information
  this->dataPtr->statusLabel = new QLabel("Ready");
  this->dataPtr->statusLabel->setObjectName("dataLoggerStatusLabel");
  this->dataPtr->statusLabel->setStyleSheet(
      "QLabel{padding-left: 14px; padding-top: 9px}");

  // Duration of logging
  this->dataPtr->timeLabel = new QLabel("00:00:00.000");
  this->dataPtr->timeLabel->setObjectName("dataLoggerTimeLabel");
  this->dataPtr->timeLabel->setFixedWidth(90);

  // Size of log file
  this->dataPtr->sizeLabel = new QLabel("(0.00 B)");
  this->dataPtr->sizeLabel->setObjectName("dataLoggerSizeLabel");

  // Horizontal separator
  QFrame *separator = new QFrame();
  separator->setFrameShape(QFrame::HLine);
  separator->setLineWidth(1);
  separator->setFixedHeight(30);

  // Address label
  QLabel *uriLabel = new QLabel("Address: ");
  uriLabel->setStyleSheet(
      "QLabel {color: #aeaeae; font-size: 11px; background: transparent}");

  this->dataPtr->recordResourcesBox = new QCheckBox("Record model resources");
  this->dataPtr->recordResourcesBox->setChecked(false);
  connect(this->dataPtr->recordResourcesBox, SIGNAL(clicked(bool)), this,
    SLOT(OnRecordResources(bool)));
  this->dataPtr->recordResourcesBox->setStyleSheet(
    "QCheckBox {color: #aeaeae; font-size: 11px; background: transparent}");

  // Address URI Line Edit
  this->dataPtr->destURI = new QLineEdit;
  this->dataPtr->destURI->setReadOnly(true);
  this->dataPtr->destURI->setObjectName("dataLoggerDestnationURILabel");
  this->dataPtr->destURI->setStyleSheet(
      "QLineEdit {color: #aeaeae; font-size: 11px; background: transparent}");

  // "Save to" label
  QLabel *pathLabel = new QLabel("Save to: ");
  pathLabel->setStyleSheet(
      "QLabel {\
         color: #aeaeae;\
         font-size: 11px;\
         background: transparent;\
         padding-top: 4px;\
       }");

  // Destination path Text Edit
  this->dataPtr->destPath = new QPlainTextEdit();
  this->dataPtr->destPath->setObjectName("dataLoggerDestnationPathLabel");
  this->dataPtr->destPath->setMinimumWidth(200);
  this->dataPtr->destPath->setMaximumHeight(50);
  this->dataPtr->destPath->setReadOnly(true);
  this->dataPtr->destPath->setFrameStyle(QFrame::NoFrame);
  this->dataPtr->destPath->setLineWrapMode(QPlainTextEdit::WidgetWidth);
  this->dataPtr->destPath->setWordWrapMode(QTextOption::WrapAnywhere);
  this->dataPtr->destPath->setStyleSheet(
      "QPlainTextEdit {\
         color: #aeaeae;\
         font-size: 11px;\
         background: transparent;\
       }");

  // Browser button
  QPushButton *browseButton = new QPushButton("Browse");
  browseButton->setFixedWidth(100);
  browseButton->setFocusPolicy(Qt::NoFocus);
  connect(browseButton, SIGNAL(clicked()), this, SLOT(OnBrowse()));

  // Button which toggles recordings
  QRadioButton *recordingsButton = new QRadioButton();
  recordingsButton->setChecked(false);
  recordingsButton->setFocusPolicy(Qt::NoFocus);
  recordingsButton->setText("Recordings");
  recordingsButton->setStyleSheet(
     "QRadioButton {\
        color: #d0d0d0;\
      }\
      QRadioButton::indicator::unchecked {\
        image: url(:/images/right_arrow.png);\
      }\
      QRadioButton::indicator::checked {\
        image: url(:/images/down_arrow.png);\
      }");
  connect(recordingsButton, SIGNAL(toggled(bool)), this,
      SLOT(OnToggleSettings(bool)));

  // Insert widgets in the top layout
  QGridLayout *topLayout = new QGridLayout();
  topLayout->addWidget(this->dataPtr->recordButton, 0, 0, 2, 1);
  topLayout->addWidget(this->dataPtr->statusLabel, 0, 1, 2, 2);
  topLayout->addWidget(this->dataPtr->timeLabel, 0, 3);
  topLayout->addWidget(this->dataPtr->sizeLabel, 1, 3);
  topLayout->addWidget(separator, 2, 0, 1, 4);
  topLayout->addWidget(this->dataPtr->recordResourcesBox, 3, 0, 1, 4);
  topLayout->addWidget(uriLabel, 4, 0);
  topLayout->addWidget(this->dataPtr->destURI, 4, 1, 1, 3);
  topLayout->addWidget(pathLabel, 5, 0);
  topLayout->addWidget(this->dataPtr->destPath, 5, 1, 1, 2);
  topLayout->addWidget(browseButton, 5, 3);
  topLayout->addWidget(recordingsButton, 6, 0, 1, 4);

  // Align widgets within layout
  topLayout->setAlignment(this->dataPtr->recordResourcesBox, Qt::AlignLeft);
  topLayout->setAlignment(pathLabel, Qt::AlignTop | Qt::AlignRight);
  topLayout->setAlignment(browseButton, Qt::AlignTop);
  topLayout->setAlignment(this->dataPtr->destURI, Qt::AlignLeft);
  topLayout->setAlignment(this->dataPtr->destPath, Qt::AlignLeft);
  topLayout->setAlignment(this->dataPtr->statusLabel, Qt::AlignLeft);
  topLayout->setAlignment(this->dataPtr->timeLabel, Qt::AlignRight);
  topLayout->setAlignment(this->dataPtr->sizeLabel,
      Qt::AlignRight | Qt::AlignTop);
  topLayout->setAlignment(uriLabel, Qt::AlignRight);

  // Put the layout in a widget to be able to control size
  QWidget *topWidget = new QWidget();
  topWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
  topWidget->setLayout(topLayout);

  // List of recorded logs
  this->dataPtr->logList = new QTextBrowser(this);
  this->dataPtr->logList->setObjectName("dataLoggerRecordingsList");

  // Layout to hold the list
  QVBoxLayout *settingsLayout = new QVBoxLayout;
  settingsLayout->setContentsMargins(2, 2, 2, 2);
  settingsLayout->addWidget(this->dataPtr->logList);

  // Frame that can be hidden by toggling the expand button
  this->dataPtr->settingsFrame = new QFrame();
  this->dataPtr->settingsFrame->setObjectName("dataLoggerSettingFrame");
  this->dataPtr->settingsFrame->setLayout(settingsLayout);
  this->dataPtr->settingsFrame->setSizePolicy(QSizePolicy::Expanding,
      QSizePolicy::Expanding);
  this->dataPtr->settingsFrame->hide();

  // Mainlayout for the whole widget
  QVBoxLayout *mainLayout = new QVBoxLayout;
  mainLayout->setSizeConstraint(QLayout::SetMinAndMaxSize);
  mainLayout->addWidget(topWidget);
  mainLayout->addWidget(this->dataPtr->settingsFrame);

  // Assign the mainlayout to this widget
  this->setLayout(mainLayout);

  // Create a QueuedConnection to set time. This is used for thread safety.
  connect(this, SIGNAL(SetTime(QString)), this->dataPtr->timeLabel,
      SLOT(setText(QString)), Qt::QueuedConnection);

  // Create a QueuedConnection to set size. This is used for thread safety.
  connect(this, SIGNAL(SetSize(QString)), this->dataPtr->sizeLabel,
      SLOT(setText(QString)), Qt::QueuedConnection);

  // Create a QueuedConnection to set destination path.
  // This is used for thread safety.
  connect(this, SIGNAL(SetDestinationPath(QString)),
          this, SLOT(OnSetDestinationPath(QString)), Qt::QueuedConnection);

  // Create a QueuedConnection to set destination URI.
  // This is used for thread safety.
  connect(this, SIGNAL(SetDestinationURI(QString)),
          this, SLOT(OnSetDestinationURI(QString)), Qt::QueuedConnection);

  connect(this, SIGNAL(rejected()), this, SLOT(OnCancel()));

  // Timer used to blink the status label
  this->dataPtr->statusTimer = new QTimer();
  connect(this->dataPtr->statusTimer,
      SIGNAL(timeout()), this, SLOT(OnBlinkStatus()));
  this->dataPtr->statusTime = 0;

  // Timer used to hide the confirmation dialog
  this->dataPtr->confirmationDialog = NULL;
  this->dataPtr->confirmationTimer = new QTimer(this);
  connect(this->dataPtr->confirmationTimer, SIGNAL(timeout()), this,
      SLOT(OnConfirmationTimeout()));

  // Create a node from communication.
  this->dataPtr->node = transport::NodePtr(new transport::Node());
  this->dataPtr->node->TryInit(common::Time::Maximum());

  // Advertise on the log control topic. The server listens to log control
  // messages.
  this->dataPtr->pub =
      this->dataPtr->node->Advertise<msgs::LogControl>("~/log/control");

  // Subscribe to the log status topic. The server publishes log status
  // messages.
  this->dataPtr->sub = this->dataPtr->node->Subscribe<msgs::LogStatus>(
      "~/log/status",  &DataLogger::OnStatus, this);

  // Fill the path with the home folder - duplicated from util/LogRecord
#ifndef _WIN32
  const char *homePath = common::getEnv("HOME");
#else
  const char *homePath = common::getEnv("HOMEPATH");
#endif

  GZ_ASSERT(homePath, "HOME environment variable is missing");

  if (!homePath)
  {
    common::SystemPaths *paths = common::SystemPaths::Instance();
    this->dataPtr->basePath =
        QString::fromStdString(paths->TmpPath() + "/gazebo");
  }
  else
  {
    this->dataPtr->basePath =
        QString::fromStdString(boost::filesystem::path(homePath).string());
  }

  this->dataPtr->basePath = this->dataPtr->basePath + "/.gazebo/log/";
  this->SetDestinationPath(this->dataPtr->basePath);
}

/////////////////////////////////////////////////
DataLogger::~DataLogger()
{
  this->dataPtr->sub.reset();
  this->dataPtr->pub.reset();
  if (this->dataPtr->node)
    this->dataPtr->node->Fini();
  this->dataPtr->node.reset();
}

/////////////////////////////////////////////////
void DataLogger::OnRecord(bool _toggle)
{
  // If _toggle, then we should start logging data.
  if (_toggle)
  {
    // Switch the icon
    this->dataPtr->recordButton->setIcon(QPixmap(":/images/record_stop.png"));

    this->dataPtr->statusLabel->setText("Recording...");
    this->dataPtr->statusTimer->start(100);

    // Tell the server to start data logging
    msgs::LogControl msg;
    msg.set_start(true);
    this->dataPtr->pub->Publish(msg);
  }
  // Otherwise pause data logging
  else
  {
    // Switch the icon
    this->dataPtr->recordButton->setIcon(QPixmap(":/images/record.png"));

    // Display confirmation
    this->dataPtr->confirmationTimer->start(2000);
    QLabel *confirmationLabel = new QLabel("Saved to \n" +
        this->dataPtr->destPath->toPlainText());
    confirmationLabel->setObjectName("dataLoggerConfirmationLabel");
    QHBoxLayout *confirmationLayout = new QHBoxLayout();
    confirmationLayout->addWidget(confirmationLabel);

    if (this->dataPtr->confirmationDialog)
      this->dataPtr->confirmationDialog->close();
    this->dataPtr->confirmationDialog =
        new QDialog(this, Qt::FramelessWindowHint);
    this->dataPtr->confirmationDialog->setObjectName(
        "dataLoggerConfirmationDialog");
    this->dataPtr->confirmationDialog->setLayout(confirmationLayout);
    this->dataPtr->confirmationDialog->setStyleSheet(
        "QDialog {background-color: #eee}\
         QLabel {color: #111}");
    this->dataPtr->confirmationDialog->setModal(false);
    this->dataPtr->confirmationDialog->show();
    this->dataPtr->confirmationDialog->move(this->mapToGlobal(
        QPoint((this->width()-this->dataPtr->confirmationDialog->width())*0.5,
        this->height() + 10)));

    // Change the status
    this->dataPtr->statusLabel->setText("Ready");
    this->dataPtr->statusLabel->setStyleSheet(
        "QLabel {\
           color: #aeaeae;\
           padding-left: 14px;\
           padding-top: 9px;\
         }");
    this->dataPtr->statusTimer->stop();

    // Change the Save to box
    this->SetDestinationPath(this->dataPtr->basePath);

    // Tell the server to stop data logging
    msgs::LogControl msg;
    msg.set_stop(true);
    this->dataPtr->pub->Publish(msg);

    this->dataPtr->logList->append(this->dataPtr->destPath->toPlainText());
  }
}

/////////////////////////////////////////////////
void DataLogger::OnStatus(ConstLogStatusPtr &_msg)
{
  // A new log status message has arrived, let's display the contents.
  common::Time time = msgs::Convert(_msg->sim_time());
  std::ostringstream stream;

  // Compute the hours, minutes, seconds, and milliseconds that logging has
  // been running.
  unsigned int hours = time.sec / 3600;
  unsigned int min = (time.sec - hours * 3600) / 60;
  unsigned int sec = (time.sec - hours * 3600  - min * 60);
  unsigned int msec = rint(time.nsec * 1e-6);

  // Display the time.
  stream << std::setw(2) << std::setfill('0') << hours  << ":";
  stream << std::setw(2) << std::setfill('0') << min << ":";
  stream << std::setw(2) << std::setfill('0') << sec << ".";
  stream << std::setw(3) << std::setfill('0') << msec;

  this->SetTime(QString::fromStdString(stream.str()));

  // Reset the stream
  stream.str("");

  // If there is log file information in the message...
  if (_msg->has_log_file())
  {
    // If there is file name information and we're recording...
    if (_msg->log_file().has_base_path() &&
        this->dataPtr->recordButton->isChecked())
    {
      std::string logBasePath = _msg->log_file().base_path();

      // Display the log path
      if (_msg->log_file().has_full_path() && !logBasePath.empty())
      {
        std::string fullPath = _msg->log_file().full_path();
        if (!fullPath.empty())
          this->SetDestinationPath(QString::fromStdString(fullPath));
      }
    }

    // Display the URI
    if (_msg->log_file().has_uri())
      this->SetDestinationURI(QString::fromStdString(_msg->log_file().uri()));
    else
      this->SetDestinationURI(tr(""));

    // If there is log file size information...
    if (_msg->log_file().has_size() && _msg->log_file().has_size_units())
    {
      // Get the size of the log file.
      stream << std::fixed << std::setprecision(2) << "(" <<
          _msg->log_file().size();

      // Get the size units.
      switch (_msg->log_file().size_units())
      {
        case msgs::LogStatus::LogFile::BYTES:
          stream << "B)";
          break;
        case msgs::LogStatus::LogFile::K_BYTES:
          stream << "KB)";
          break;
        case msgs::LogStatus::LogFile::M_BYTES:
          stream << "MB)";
          break;
        default:
          stream << "GB)";
          break;
      }

      this->SetSize(QString::fromStdString(stream.str()));
    }
    else
      this->SetSize("(0.00 B)");
  }
}

/////////////////////////////////////////////////
void DataLogger::OnSetDestinationPath(QString _filename)
{
  if (!_filename.isEmpty())
    this->dataPtr->destPath->setPlainText(_filename);
}

/////////////////////////////////////////////////
void DataLogger::OnSetDestinationURI(QString _uri)
{
  if (!_uri.isEmpty())
    this->dataPtr->destURI->setText(_uri);
  else
    this->dataPtr->destURI->setText("");
}

/////////////////////////////////////////////////
void DataLogger::OnToggleSettings(bool _checked)
{
  if (_checked)
    this->dataPtr->settingsFrame->show();
  else
    this->dataPtr->settingsFrame->hide();
}

/////////////////////////////////////////////////
void DataLogger::OnRecordResources(bool _checked)
{
  msgs::LogControl msg;
  msg.set_record_resources(_checked);
  this->dataPtr->pub->Publish(msg);
}

/////////////////////////////////////////////////
void DataLogger::OnBrowse()
{
  QFileDialog fileDialog(this, tr("Set log directory"), QDir::homePath());
  fileDialog.setFileMode(QFileDialog::Directory);
  fileDialog.setFilter(QDir::AllDirs | QDir::Hidden);
  fileDialog.setOptions(QFileDialog::ShowDirsOnly
      | QFileDialog::DontResolveSymlinks);
  fileDialog.setWindowFlags(Qt::Window | Qt::WindowCloseButtonHint |
      Qt::WindowStaysOnTopHint | Qt::CustomizeWindowHint);

  if (fileDialog.exec() != QDialog::Accepted)
    return;

  QStringList selected = fileDialog.selectedFiles();
  if (selected.empty())
    return;

  boost::filesystem::path path = selected[0].toStdString();

  // Make sure the  directory exists
  if (!boost::filesystem::exists(path))
  {
    QMessageBox msgBox(this);
    std::ostringstream stream;
    stream << "Directory " << path << " does not exist.";
    msgBox.setText(stream.str().c_str());
    msgBox.exec();
    return;
  }

  // Make sure we have a directory
  if (!boost::filesystem::is_directory(path))
  {
    QMessageBox msgBox(this);
    std::ostringstream stream;
    stream << "Path " << path << " is not a directory. Please only specify a "
           << "directory for data logging.";
    msgBox.setText(stream.str().c_str());
    msgBox.exec();
    return;
  }

  // Make sure the path is writable.
  // Note: This is not cross-platform compatible.
#ifdef _WIN32
  // Check for write-only (2) and read-write (6)
  if ((_access(path.string().c_str(), 2) != 0) &&
      (_access(path.string().c_str(), 6) != 0))
#else
  if (access(path.string().c_str(), W_OK) != 0)
#endif
  {
    QMessageBox msgBox(this);
    std::ostringstream stream;
    stream << "You do no have permission to write into " << path;
    msgBox.setText(stream.str().c_str());
    msgBox.exec();
    return;
  }

  // Set the new base path
  msgs::LogControl msg;
  msg.set_base_path(path.string());
  this->dataPtr->pub->Publish(msg);

  this->dataPtr->basePath = QString::fromStdString(path.string());
  this->SetDestinationPath(this->dataPtr->basePath);
}

/////////////////////////////////////////////////
void DataLogger::OnBlinkStatus()
{
  this->dataPtr->statusTime += 1.0/10;

  if (this->dataPtr->statusTime >= 1)
    this->dataPtr->statusTime = 0;

  this->dataPtr->statusLabel->setStyleSheet(QString::fromStdString(
      "QLabel{color: rgb("+
          std::to_string(255+(128*(this->dataPtr->statusTime-1)))+", "+
          std::to_string(255+(128*(this->dataPtr->statusTime-1)))+", "+
          std::to_string(255+(128*(this->dataPtr->statusTime-1)))+
      "); "+
      "padding-left: 15px; padding-top: 9px}"));
}

/////////////////////////////////////////////////
void DataLogger::OnConfirmationTimeout()
{
  this->dataPtr->confirmationDialog->close();
  this->dataPtr->confirmationTimer->stop();
}

/////////////////////////////////////////////////
void DataLogger::OnCancel()
{
  if (this->dataPtr->recordButton->isChecked())
    this->dataPtr->recordButton->click();
}
