/***************************** LICENSE START ***********************************

 Copyright 2014 ECMWF and INPE. This software is distributed under the terms
 of the Apache License version 2.0. In applying this license, ECMWF does not
 waive the privileges and immunities granted to it by virtue of its status as
 an Intergovernmental Organization or submit itself to any jurisdiction.

 ***************************** LICENSE END *************************************/
#include "MvQWeatherRoomDialog.h"

#include <QComboBox>
#include <QDialogButtonBox>
#include <QFile>
#include <QImageReader>
#include <QLabel>
#include <QLineEdit>
#include <QMessageBox>
#include <QMouseEvent>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QPainter>
#include <QSettings>
#include <QTextStream>
#include <QVBoxLayout>
#include "MvQMethods.h"

#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/json_parser.hpp>


MvQVideoWallCellInfo::MvQVideoWallCellInfo(double x, double y, double w, double h, int number)
{
    x_ = x;
    y_ = y;
    w_ = w;
    h_ = h;
    number_ = number;
}


//===========================================
//
// MvQVideoWallServer
//
//============================================

MvQVideoWallServer::MvQVideoWallServer(QObject* parent) :
    QObject(parent),
    wallInfo_(0)
{
    QNetworkAccessManager* manager = new QNetworkAccessManager(this);

    connect(manager, SIGNAL(finished(QNetworkReply*)),
            this, SLOT(slotWallServerReply(QNetworkReply*)));

    const char* caux = getenv("METVIEW_WALL_URL");
    if (!caux) {
        QMessageBox::warning(0, QString("Metview"),
                             tr("Env var METVIEW_WALL_URL is not defined"));
        return;
    }
    URL_ = caux;

    caux = getenv("METVIEW_WALL_TOKEN");
    if (!caux) {
        QMessageBox::warning(0, QString("Metview"),
                             tr("Env var METVIEW_WALL_TOKEN is not defined"));
        return;
    }
    token_ = caux;

    QString req = QString("%1/get-wall-list/?key=%2").arg(URL_).arg(token_);

    QNetworkRequest netReq(QUrl::fromPercentEncoding(req.toUtf8()));

    manager->get(netReq);
}

void MvQVideoWallServer::slotWallServerReply(QNetworkReply* reply)
{
    if (reply->error() != QNetworkReply::NoError) {
        QMessageBox::warning(0, QString("Metview"),
                             tr("Unable to get server response.\n %1").arg(reply->errorString()));

        reply->deleteLater();
        return;
    }

    // Get response from the Wall Info request
    QByteArray ba = reply->readAll();
    QString res(ba);

    // Retrieve list of walls
    if (!this->parseWallServer(res)) {
        reply->deleteLater();
        return;
    }

    emit gotServerResponse();

    reply->deleteLater();
}

bool MvQVideoWallServer::parseWallServer(QString& response)
{
    // Write the response to a temporary text file
    QString tmpFilename(marstmp());
    QFile tmpFile(tmpFilename);
    tmpFile.open(QIODevice::WriteOnly);

    // Check if the temporary file was opened
    if (!tmpFile.isOpen()) {
        QMessageBox::warning(0, QString("Metview"),
                             tr("Unable to open temporary file %1").arg(tmpFilename));
        return false;
    }

    // Write the server response to the file
    QTextStream outStream(&tmpFile);
    outStream << response;
    tmpFile.close();

    // Parse the response using the boost JSON property tree parser
    using boost::property_tree::ptree;
    ptree pt;
    std::string tmpFilenameStd = tmpFilename.toUtf8().constData();
    try {
        read_json(tmpFilenameStd, pt);
    }
    catch (const boost::property_tree::json_parser::json_parser_error& e) {
        QMessageBox::warning(0, QString("Metview"),
                             tr("Unable to parse JSON response from the server"));
        return false;
    }

    // Get list of walls
    wallNames_.clear();
    wallIds_.clear();
    for (ptree::const_iterator itOuter = pt.begin(); itOuter != pt.end(); ++itOuter) {
        if (itOuter->first != "walls")
            continue;

        ptree const& layout = itOuter->second;
        MvQVideoWallInfo wallInfo;

        // Loop walls
        for (ptree::const_iterator itLayout = layout.begin(); itLayout != layout.end(); ++itLayout) {
            ptree const& layout = itLayout->second;

            // For a given wall, find the title and name info
            for (ptree::const_iterator itRows = layout.begin(); itRows != layout.end(); ++itRows) {
                if (itRows->first == "title")
                    wallNames_.push_back(itRows->second.get_value<std::string>("name"));
                else if (itRows->first == "name")
                    wallIds_.push_back(itRows->second.get_value<std::string>("name"));
            }
        }
    }

    // Consistency check
    if (wallNames_.empty() || wallNames_.size() != wallIds_.size()) {
        QMessageBox::warning(0, QString("Metview"),
                             tr("Problem parsing JSON file: %1").arg(tmpFilename));
        return false;
    }

    return true;
}

void MvQVideoWallServer::createWallLayout(int index)
{
    // Build and call a network request to inquiry about a layout
    // related to the input index.
    // Instantiate a network
    QNetworkAccessManager* manager = new QNetworkAccessManager(this);
    connect(manager, SIGNAL(finished(QNetworkReply*)),
            this, SLOT(slotWallLayoutReply(QNetworkReply*)));

    QString aux = wallIds_[index].c_str();
    QString req = QString("%1/wall-status/?key=%2&json={\"wall\":\"%3\"}").arg(URL_).arg(token_).arg(aux);

    QNetworkRequest netReq(QUrl::fromPercentEncoding(req.toUtf8()));

    manager->get(netReq);
}

void MvQVideoWallServer::slotWallLayoutReply(QNetworkReply* reply)
{
    if (reply->error() != QNetworkReply::NoError) {
        QMessageBox::warning(0, QString("Metview"),
                             tr("Unable to get wall layout response.\n %1").arg(reply->errorString()));

        // Clear layout
        if (wallInfo_) {
            delete wallInfo_;
            wallInfo_ = 0;
        }

        emit gotServerLayoutResponse();

        reply->deleteLater();
        return;
    }

    // Get response from the wall layout request
    QByteArray ba = reply->readAll();
    QString res(ba);

    // Retrieve layout
    if (!this->parseWallLayout(res)) {
        reply->deleteLater();
        return;
    }

    emit gotServerLayoutResponse();

    reply->deleteLater();
}

bool MvQVideoWallServer::parseWallLayout(QString& response)
{
    // Write the response to a temporary text file
    QString tmpFilename(marstmp());
    QFile tmpFile(tmpFilename);
    tmpFile.open(QIODevice::WriteOnly);

    // Check if the temporary file was opened
    if (!tmpFile.isOpen()) {
        QMessageBox::warning(0, QString("Metview"),
                             tr("Unable to open temporary file: %1").arg(tmpFilename));
        return false;
    }

    // Write the server response to the file
    QTextStream outStream(&tmpFile);
    outStream << response;
    tmpFile.close();

    // Parse the response using the boost JSON property tree parser
    using boost::property_tree::ptree;
    ptree pt;
    std::string tmpFilenameStd = tmpFilename.toUtf8().constData();
    try {
        read_json(tmpFilenameStd, pt);
    }
    catch (const boost::property_tree::json_parser::json_parser_error& e) {
        QMessageBox::warning(0, QString("Metview"),
                             tr("Unable to parse JSON response from the server"));
        return false;
    }

    // Main Loop
    for (ptree::const_iterator itOuter = pt.begin(); itOuter != pt.end(); ++itOuter) {
        if (itOuter->first != "config")
            continue;

        // Start a new wall layout. There is only one layout to be built.
        if (wallInfo_)
            delete wallInfo_;

        wallInfo_ = new MvQVideoWallInfo;

        // Process the config tag
        ptree const& layout = itOuter->second;
        for (ptree::const_iterator itLayout = layout.begin(); itLayout != layout.end(); ++itLayout) {
            if (itLayout->first != "layout")
                continue;

            // Find out the layout design (shape and number of cells) by
            // looping each cell and looking for colspan and rowspan
            int totalRows = 0;  // must take into account the rowspans
            int totalCols = 0;  // must take into account the colspans
            int r = 0;
            ptree const& layout = itLayout->second;
            for (ptree::const_iterator itRows = layout.begin(); itRows != layout.end(); ++itRows) {
                // For each tag in the current cell
                ptree const& row = itRows->second;
                int c = 0;
                for (ptree::const_iterator itCols = row.begin(); itCols != row.end(); ++itCols) {
                    int colSpan = itCols->second.get<int>("colspan", 1);
                    int rowSpan = itCols->second.get<int>("rowspan", 1);

                    if (r + rowSpan > totalRows)
                        totalRows = r + rowSpan;

                    if (c + colSpan > totalCols)
                        totalCols = c + colSpan;

                    c += colSpan;
                }
                r += 1;
            }

            // Build the Qt layout by looping each cell for the second time
            int cellNumber = -1;
            double rowHeight = 1.0 / totalRows;
            double currentY = 0.0;

            for (ptree::const_iterator itRows = layout.begin(); itRows != layout.end(); ++itRows) {
                ptree const& row = itRows->second;
                double colWidth = 1.0 / totalCols;
                double currentX = 0.0;

                // For each tag in the current cell
                for (ptree::const_iterator itCols = row.begin(); itCols != row.end(); ++itCols) {
                    std::string cellStr = itCols->second.get<std::string>("name", "no-nameX");
                    if (cellStr != "no-nameX") {
                        int colSpan = itCols->second.get<int>("colspan", 1);
                        int rowSpan = itCols->second.get<int>("rowspan", 1);

                        cellNumber++;
                        MvQVideoWallCellInfo cellInfo(currentX, currentY, colWidth * colSpan, rowHeight * rowSpan, cellNumber);
                        wallInfo_->addCellInfo(cellInfo);

                        // set up the x-coord for the next column
                        currentX += colWidth * colSpan;
                    }
                    else
                        currentX += colWidth;
                }

                // set up the y-coord for the next row
                currentY += rowHeight;
            }

            break;  // Wall layout has been created
        }
    }

    return true;
}

// Returns -1 if a wall of that name does not exist
int MvQVideoWallServer::wallInfoIndexFromName(const std::string& wallName)
{
    if (wallName.empty())
        return -1;

    for (size_t i = 0; i < numWalls(); i++) {
        if (wallNames_[i] == wallName)
            return i;
    }

    // No wall found
    return -1;
}

// If the wallName does not exist, returns the first Id
std::string MvQVideoWallServer::wallInfoId(const std::string& wallName)
{
    int index = wallInfoIndexFromName(wallName);

    return (index == -1) ? wallIds_[0] : wallIds_[index];
}

//===========================================
//
// MvQVideoWallWidget
//
//============================================

MvQVideoWallWidget::MvQVideoWallWidget(QWidget* parent) :
    QWidget(parent),
    offset_(1),
    current_(0)
{
    setSizePolicy(QSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum));
    setMinimumSize(QSize(128, 128));

    setMouseTracking(true);

    int size = 128;
    QImageReader imgR(":/uPlot/wall_screen.svg");
    if (imgR.canRead()) {
        imgR.setScaledSize(QSize(size, size));
        QImage img = imgR.read();
        pix_ = QPixmap::fromImage(img);
    }

    QImageReader imgRs(":/uPlot/wall_screen_selected.svg");
    if (imgRs.canRead()) {
        imgRs.setScaledSize(QSize(size, size));
        QImage img = imgRs.read();
        pixSelect_ = QPixmap::fromImage(img);
    }
}

// Populates the video wall widget with the screens from the given wall info
void MvQVideoWallWidget::setWall(MvQVideoWallInfo* info)
{
    if (!info)
        items_ << QRect(0, 0, 100, 100);
    else {
        items_.clear();
        for (unsigned int i = 0; i < info->cellInfos().size(); i++) {
            MvQVideoWallCellInfo* cell = &(info->cellInfos()[i]);
            items_ << QRect(cell->x() * 100., cell->y() * 100., cell->w() * 100., cell->h() * 100.);
        }
        setEnabled(true);
    }

    update();
}

void MvQVideoWallWidget::paintEvent(QPaintEvent*)
{
    int w = width() - 2 * offset_;
    int h = height() - 2 * offset_;

    QPainter painter(this);

    if (!isEnabled()) {
        QString message("Retrieving layout from server...");
        QTextOption textOptions;
        textOptions.setAlignment(Qt::AlignHCenter);
        textOptions.setWrapMode(QTextOption::WordWrap);

        painter.drawText(QRect(0, 0, w, h), message, textOptions);
        return;
    }

    QColor col(128, 128, 255);
    QBrush brush;
    brush.setStyle(Qt::SolidPattern);

    painter.setPen(Qt::black);

    for (int i = 0; i < items_.count(); i++) {
        QRectF r(offset_ + (w * items_[i].x()) * 0.01, offset_ + (h * items_[i].y()) * 0.01,
                 (w * items_[i].width()) * 0.01, (h * items_[i].height()) * 0.01);

        if (i == current_)
            painter.drawPixmap(r, pixSelect_, QRectF(0, 0, pixSelect_.width(), pixSelect_.height()));
        else
            painter.drawPixmap(r, pix_, QRectF(0, 0, pix_.width(), pix_.height()));
    }
}

// Selects a screen in the video wall; called, for instance, when the user
// types a new screen number into the text box
void MvQVideoWallWidget::updateCellSelection(const QString& selection)
{
    int screenNum = selection.toInt();
    current_ = screenNum - 1;
    update();
}


void MvQVideoWallWidget::mousePressEvent(QMouseEvent* event)
{
    if (select(event->pos())) {
        update();
        QString newSelection;
        newSelection.setNum(current_ + 1);
        emit selectionChanged(newSelection);
    }
}

bool MvQVideoWallWidget::select(QPoint pos)
{
    int x = (pos.x() - offset_) * 100 / width();
    int y = (pos.y() - offset_) * 100 / height();

    for (int i = 0; i < items_.count(); i++) {
        if (items_.at(i).contains(QPoint(x, y))) {
            current_ = i;
            return true;
        }
    }

    return false;
}

//===========================================
//
// MvQWeatherRoomDialog
//
//============================================

MvQWeatherRoomDialog::MvQWeatherRoomDialog(int currentFrame, int totalFrames, QWidget* parent) :
    QDialog(parent),
    currentFrame_(currentFrame),
    totalFrames_(totalFrames),
    lastWall_(""),
    lastCell_(1),
    updatingFromServer_(true)
{
    // Wall combo
    QVBoxLayout* vb = new QVBoxLayout();
    setLayout(vb);
    QLabel* label = new QLabel(tr("Video wall:"), this);
    wallCb_ = new QComboBox(this);
    QHBoxLayout* hb = new QHBoxLayout();
    hb->addWidget(label);
    hb->addWidget(wallCb_, 1);
    vb->addLayout(hb);

    // Initialize wall server
    wallServer_ = new MvQVideoWallServer(this);
    connect(wallServer_, SIGNAL(gotServerResponse()), this, SLOT(updateWallServerSelector()));
    connect(wallServer_, SIGNAL(gotServerLayoutResponse()), this, SLOT(updateWallLayoutSelector()));
    connect(wallCb_, SIGNAL(currentIndexChanged(int)), this, SLOT(updateWallSelection(int)));
    wallCb_->addItem("No wall");

    // Cell selector
    cellSelector_ = new MvQVideoWallWidget(this);
    vb->addWidget(cellSelector_, 1);
    cellSelector_->setWall(nullptr);
    cellSelector_->setEnabled(false);

    // Cell selection text box (for if the layout plot is not sufficient)
    QHBoxLayout* hb2 = new QHBoxLayout();
    QLabel* screenSelectionLabel = new QLabel(tr("Cell:"), this);
    hb2->addWidget(screenSelectionLabel);
    leCellSelection_ = new QLineEdit(this);
    QValidator* intValidator = new QIntValidator(1, 100, this);
    leCellSelection_->setValidator(intValidator);
    hb2->addWidget(leCellSelection_);
    vb->addLayout(hb2);

    // Two-way connection between the screen selection widget and text box
    connect(leCellSelection_, SIGNAL(textEdited(const QString&)),
            cellSelector_, SLOT(updateCellSelection(const QString&)));

    connect(cellSelector_, SIGNAL(selectionChanged(const QString&)),
            leCellSelection_, SLOT(setText(const QString&)));

    // Frame selection
    // buildFrameSelection(vb);

    // Buttonbox
    QDialogButtonBox* buttonBox = new QDialogButtonBox(this);
    buttonBox->addButton(QDialogButtonBox::Ok);
    buttonBox->addButton(QDialogButtonBox::Cancel);

    connect(buttonBox, SIGNAL(accepted()), this, SLOT(accept()));
    connect(buttonBox, SIGNAL(rejected()), this, SLOT(reject()));

    vb->addWidget(buttonBox);

    // Read the previous gui settings
    readSettings();

    // Initialise the text box
    QString newSelection;
    newSelection.setNum(lastCell_);
    leCellSelection_->setText(newSelection);
}

// Destructor
MvQWeatherRoomDialog::~MvQWeatherRoomDialog()
{
    writeSettings();
}

void MvQWeatherRoomDialog::setVisible(bool b)
{
    QDialog::setVisible(b);
}

void MvQWeatherRoomDialog::updateWallSelection(int wallIndex)
{
    if (!updatingFromServer_ && (wallIndex > -1) && (wallServer_->numWalls() > 0))
        wallServer_->createWallLayout(wallIndex);
}

void MvQWeatherRoomDialog::updateWallServerSelector()
{
    // Update the list of walls
    updatingFromServer_ = true;
    wallCb_->clear();
    for (size_t i = 0; i < wallServer_->numWalls(); i++) {
        QString name = wallServer_->wallInfoName(i).c_str();
        if (!name.isEmpty())
            wallCb_->addItem(name);
    }

    // Get the wall index. Previous one if exists or the first one by default
    int lastWallIndex = wallServer_->wallInfoIndexFromName(lastWall_);
    if (lastWallIndex < 0) {
        lastCell_ = 1;  // default value
        lastWallIndex = 0;
    }

    // Update drop down menu interface
    wallCb_->setCurrentIndex(lastWallIndex);
    updatingFromServer_ = false;  // we've finished updating the list

    // Call a second network request to get the wall layout
    wallServer_->createWallLayout(lastWallIndex);
}

void MvQWeatherRoomDialog::updateWallLayoutSelector()
{
    cellSelector_->setWall(wallServer_->wallInfo());
    setSelectedCell(this->getCell());
}

// Forces the given cell to be the selected one
void MvQWeatherRoomDialog::setSelectedCell(int icell)
{
    int cellNumber = (icell > wallServer_->numCells() || icell < 1) ? 1 : icell;
    QString newSelection;
    newSelection.setNum(cellNumber);
    leCellSelection_->setText(newSelection);
    cellSelector_->updateCellSelection(newSelection);
}

int MvQWeatherRoomDialog::getCell()
{
    int cell = leCellSelection_->text().toInt();
    return cell;
}

std::string MvQWeatherRoomDialog::getWallName()
{
    return wallCb_->currentText().toStdString();
}

std::string MvQWeatherRoomDialog::getWallId()
{
    std::string name = this->getWallName();
    return wallServer_->wallInfoId(name);
}

// Called when the user clicks Ok or <CR>
void MvQWeatherRoomDialog::accept()
{
    lastWall_ = getWallName();  // store the selections
    lastCell_ = getCell();      // store the selections

    // Check if selected wall is available
    if (!wallServer_->wallInfo()) {
        QMessageBox::warning(0, QString("Metview"), tr("Selected wall is not available.\n Please select another wall"));
        return;
    }

    // Check if cell number is valid
    if (lastCell_ < 1 || lastCell_ > cellSelector_->numCells()) {
        QString message = QString("Invalid cell number.\n Please select a value between 1 and %1").arg(cellSelector_->numCells());
        QMessageBox::warning(0, QString("Metview"), tr(message.toStdString().c_str()));
        return;
    }

    QDialog::accept();
}

void MvQWeatherRoomDialog::readSettings()
{
    // Restore previous gui settings
    QSettings settings(MVQ_QSETTINGS_DEFAULT_ARGS, "uPlot_MvQWeatherRoomDialog");
    settings.beginGroup("main");
    if (settings.contains("size"))
        resize(settings.value("size").toSize());
    else
        resize(QSize(420, 400));

    settings.endGroup();

    if (settings.contains("last_wall"))
        lastWall_ = settings.value("lastWall").toString().toStdString();

    lastCell_ = settings.value("lastCell", 1).toInt();
}

void MvQWeatherRoomDialog::writeSettings()
{
    // Save current gui settings
    QSettings settings(MVQ_QSETTINGS_DEFAULT_ARGS, "uPlot_MvQWeatherRoomDialog");
    settings.beginGroup("main");
    settings.setValue("size", size());
    settings.endGroup();

    settings.setValue("lastWall", QString(lastWall_.c_str()));
    settings.setValue("lastCell", lastCell_);
}

#if 0
void MvQWeatherRoomDialog::slotRangeFrameSelection(bool checked)
{
   // Enable/Disable line edit
   //leFs3_->setEnabled(checked);
}

void MvQWeatherRoomDialog::buildFrameSelection(QLayout *layout)
{
   // Group Box with radio buttons and one line edit
   QGroupBox* gbFs = new QGroupBox( tr("Frame Selection"),this );
   cbFs1_ = new QRadioButton( tr("&Current"), gbFs);
   cbFs2_ = new QRadioButton( tr("&All"), gbFs );
   cbFs3_ = new QRadioButton( tr("&Range"), gbFs );
   cbFs1_->setChecked(true);

   // Line edit with a validator
   leFs3_ = new QLineEdit(this);
   leFs3_->setToolTip(tr("Specify one or more frame ranges, e.g. 1-3,6,7"));
   leFs3_->setEnabled(false);
   QRegExp rx("^\\d{1,}(-\\d{1,})?(,\\d{1,}|,\\d{1,}-\\d{1,})*");
   QValidator *validator = new QRegExpValidator(rx, this);
   leFs3_->setValidator(validator);

   // Set layout
   QGridLayout* glFs = new QGridLayout( gbFs );
   glFs->addWidget( cbFs1_, 0, 0 );
   glFs->addWidget( cbFs2_, 1, 0 );
   glFs->addWidget( cbFs3_, 2, 0 );
   glFs->addWidget( leFs3_, 2, 1 );

   connect(cbFs3_,SIGNAL(toggled(bool)), this, SLOT(slotRangeFrameSelection(bool)));

   // Attach widget to the parent
   layout->addWidget(gbFs);
}

bool MvQWeatherRoomDialog::getFrameSelection( MvRequest& req )
{
   // Three options: CURRENT, ALL, RANGE
   // ALL: nothing need to be done
   if ( cbFs2_->isChecked() )
      return true;

   // CURRENT: get the current frame numbeer
   int i;
   QList<int> list;
   if ( cbFs1_->isChecked() )
   {
      list.append(current_+1);  // start from 1, not from 0
   }
   else  // RANGE
   {
      // Get the list of range entries (split by comma)
      QStringList list1 = leFs3_->text().split(",", QString::SkipEmptyParts);

      // Analyse all entries
      for ( i = 0; i < list1.size(); i++)
      {
         // Check if it is a single value or a range of values
         QStringList list2 = list1.at(i).split("-", QString::SkipEmptyParts);
         if ( list2.size() == 1 ) // single value
            list.append(list2.at(0).toInt());
         else
         {
            int i1 = list2.at(0).toInt();
            int i2 = list2.at(1).toInt();
            if ( i1 > i2 )  // invert sequence
            {
               int iaux = i1;
               i1 = i2;
               i2 = iaux;
            }
            for ( int j = i1; j <= i2; j++ )
               list.append(j);
         }
      }

      // Check if the range of values is valid
      qSort(list.begin(), list.end());
      if ( list.at(list.size()-1) > total_ )
      {
         ostrstream info;
         info << "Enter values from 1 to " << total_ << ends;
         errorMessage("Invalid range!",(const char*)info.str());
         return false;
      }
   }

   // Save list of frames selection for all drivers
   while ( req )
   {
      for ( i = 0; i < list.size(); i++)
         req.addValue("OUTPUT_FRAME_LIST",list.at(i));

      req.advance();
   }

   req.rewind();

   return true;
}

void MvQWeatherRoomDialog::errorMessage (const char* msg, const char* info)
{
   QMessageBox msgBox;
   msgBox.setIcon(QMessageBox::Critical);
   msgBox.setWindowTitle(tr("Export Error"));
   msgBox.setText(QString::fromUtf8(msg));
   if ( info )
      msgBox.setInformativeText(QString::fromUtf8(info));

   msgBox.exec();
}
#endif
