/***************************************************************************
    File                 : Matrix.cpp
    Project              : SciDAVis
    Description          : Matrix worksheet class
    --------------------------------------------------------------------
    Copyright            : (C) 2006-2009 Tilman Benkert (thzs*gmx.net)
    Copyright            : (C) 2006-2009 Knut Franke (knut.franke*gmx.de)
    Copyright            : (C) 2006-2007 Ion Vasilief (ion_vasilief*yahoo.fr)
                           (replace * with @ in the email addresses)

 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *  This program is free software; you can redistribute it and/or modify   *
 *  it under the terms of the GNU General Public License as published by   *
 *  the Free Software Foundation; either version 2 of the License, or      *
 *  (at your option) any later version.                                    *
 *                                                                         *
 *  This program 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 General Public License for more details.                           *
 *                                                                         *
 *   You should have received a copy of the GNU General Public License     *
 *   along with this program; if not, write to the Free Software           *
 *   Foundation, Inc., 51 Franklin Street, Fifth Floor,                    *
 *   Boston, MA  02110-1301  USA                                           *
 *                                                                         *
 ***************************************************************************/
#include "Matrix.h"
#include "future/matrix/MatrixView.h"
#include "ScriptEdit.h"

#include <QtGlobal>
#include <QTextStream>
#include <QList>
#include <QEvent>
#include <QContextMenuEvent>
#include <QVBoxLayout>
#include <QMouseEvent>
#include <QHeaderView>
#include <QDateTime>
#include <QApplication>
#include <QMessageBox>
#include <QVarLengthArray>
#include <QClipboard>
#include <QShortcut>
#include <QPrinter>
#include <QPrintDialog>
#include <QPainter>
#include <QLocale>
#include <QXmlStreamWriter>
#include <QtDebug>

#include <stdlib.h>
#include <math.h>
#include <stdio.h>

#include <gsl/gsl_linalg.h>
#include <gsl/gsl_math.h>

Matrix::Matrix(ScriptingEnv *env, int r, int c, const QString &label, QWidget *parent,
               const char *name, Qt::WindowFlags f)
    : MatrixView(label, parent, name, f), scripted(env)
{
    d_future_matrix = new future::Matrix(0, r, c, label);
    init(r, c);
}

Matrix::Matrix(future::Matrix *future_matrix, ScriptingEnv *env, int r, int c, const QString &label,
               QWidget *parent, const char *name, Qt::WindowFlags f)
    : MatrixView(label, parent, name, f), scripted(env)
{
    d_future_matrix = future_matrix;
    init(r, c);
}

void Matrix::init(int, int)
{
    MatrixView::setMatrix(d_future_matrix);
    d_future_matrix->setView(this);
    d_future_matrix->setNumericFormat('f');
    d_future_matrix->setDisplayedDigits(6);
    d_future_matrix->setCoordinates(1.0, 10.0, 1.0, 10.0);
    dMatrix = 0;

    birthdate = QLocale().toString(d_future_matrix->creationTime());

    // this is not very nice but works for the moment
    ui.gridLayout2->removeWidget(ui.formula_box);
    delete ui.formula_box;
    ui.formula_box = new ScriptEdit(scriptEnv, ui.formula_tab);
    ui.formula_box->setObjectName(QString::fromUtf8("formula_box"));
    ui.formula_box->setMinimumSize(QSize(60, 10));
    ui.formula_box->setAcceptRichText(false);
    ui.formula_box->setLineWrapMode(QTextEdit::WidgetWidth);
    ui.gridLayout2->addWidget(ui.formula_box, 1, 0, 1, 3);

    ui.add_cell_combobox->addItem("cell(i, j)");
    ui.add_function_combobox->addItems(scriptEnv->mathFunctions());
    updateFunctionDoc();

    connect(ui.add_function_combobox, SIGNAL(currentIndexChanged(int)), this,
            SLOT(updateFunctionDoc()));
    connect(ui.button_set_formula, SIGNAL(pressed()), this, SLOT(applyFormula()));
    connect(ui.add_function_button, SIGNAL(pressed()), this, SLOT(addFunction()));
    connect(ui.add_cell_button, SIGNAL(pressed()), this, SLOT(addCell()));

    connect(d_future_matrix, SIGNAL(columnsInserted(int, int)), this, SLOT(handleChange()));
    connect(d_future_matrix, SIGNAL(columnsRemoved(int, int)), this, SLOT(handleChange()));
    connect(d_future_matrix, SIGNAL(rowsInserted(int, int)), this, SLOT(handleChange()));
    connect(d_future_matrix, SIGNAL(rowsRemoved(int, int)), this, SLOT(handleChange()));
    connect(d_future_matrix, SIGNAL(dataChanged(int, int, int, int)), this, SLOT(handleChange()));
    connect(d_future_matrix, SIGNAL(coordinatesChanged()), this, SLOT(handleChange()));
    connect(d_future_matrix, SIGNAL(formulaChanged()), this, SLOT(handleChange()));
    connect(d_future_matrix, SIGNAL(formatChanged()), this, SLOT(handleChange()));
    connect(d_future_matrix, SIGNAL(recalculate()), this, SLOT(recalculate()));

    connect(d_future_matrix, SIGNAL(aspectDescriptionChanged(const AbstractAspect *)), this,
            SLOT(handleAspectDescriptionChange(const AbstractAspect *)));
}

Matrix::~Matrix() { }

void Matrix::handleChange()
{
    emit modifiedWindow(this);
}

double Matrix::cell(int row, int col)
{
    if (dMatrix)
        return dMatrix[row][col];
    else
        return d_future_matrix->cell(row, col);
}

void Matrix::setCell(int row, int col, double value)
{
    d_future_matrix->setCell(row, col, value);
}

void Matrix::setCells(const QVector<qreal> &data)
{
    d_future_matrix->setCells(data);
}

QString Matrix::text(int row, int col)
{
    return d_future_matrix->text(row, col);
}

void Matrix::setText(int row, int col, const QString &new_text)
{
    bool ok = true;
    QLocale locale;
    double res = locale.toDouble(new_text, &ok);
    if (ok)
        d_future_matrix->setCell(row, col, res);
    else {
        Script *script = scriptEnv->newScript(new_text, this,
                                              QString("<%1_%2_%3>").arg(name()).arg(row).arg(col));
        connect(script, SIGNAL(error(const QString &, const QString &, int)), scriptEnv,
                SIGNAL(error(const QString &, const QString &, int)));

        script->setInt(row + 1, "row");
        script->setInt(row + 1, "i");
        script->setInt(col + 1, "col");
        script->setInt(col + 1, "j");

        QVariant ret = script->eval();
        setCell(row, col, ret.toDouble());
    }
}

void Matrix::setCoordinates(double xs, double xe, double ys, double ye)
{
    d_future_matrix->setCoordinates(xs, xe, ys, ye);
}

QString Matrix::saveToString(const QString &geometry)
{
    QString s = "<matrix>\n";
    QString xml;
    QXmlStreamWriter writer(&xml);
    d_future_matrix->save(&writer);
    s += QString::number(xml.length()) + "\n"; // this is need in case there are newlines in the XML
    s += xml + "\n";
    s += geometry + "\n";
    s += "</matrix>\n";
    return s;

#if 0
	QString s = "<matrix>\n";
	s += QString(name()) + "\t";
	s += QString::number(numRows())+"\t";
	s += QString::number(numCols())+"\t";
	s += birthDate() + "\n";
	s += info;
	s += "ColWidth\t" + QString::number(columnWidth(0))+"\n";
	s += "<formula>\n" + formula() + "\n</formula>\n";
	s += "TextFormat\t" + QString(d_future_matrix->numericFormat()) + "\t" + QString::number(d_future_matrix->displayedDigits()) + "\n";
	s += "WindowLabel\t" + windowLabel() + "\t" + QString::number(captionPolicy()) + "\n";
	s += "Coordinates\t" + QString::number(xStart(),'g',15) + "\t" +QString::number(xEnd(),'g',15) + "\t";
	s += QString::number(yStart(),'g',15) + "\t" + QString::number(yEnd(),'g',15) + "\n";
	s += saveText();
	s +="</matrix>\n";
	return s;
#endif
}

QString Matrix::saveAsTemplate(const QString &info)
{
    QString s = "<matrix>\t";
    s += QString::number(numRows()) + "\t";
    s += QString::number(numCols()) + "\n";
    s += info;
    s += "ColWidth\t" + QString::number(columnWidth(0)) + "\n";
    s += "<formula>\n" + formula() + "\n</formula>\n";
    s += "TextFormat\t" + QString(d_future_matrix->numericFormat()) + "\t"
            + QString::number(d_future_matrix->displayedDigits()) + "\n";
    s += "Coordinates\t" + QString::number(xStart(), 'g', 15) + "\t"
            + QString::number(xEnd(), 'g', 15) + "\t";
    s += QString::number(yStart(), 'g', 15) + "\t" + QString::number(yEnd(), 'g', 15) + "\n";
    s += "</matrix>\n";
    return s;
}

QString Matrix::saveText()
{
    QString out_text = "<data>\n";
    int cols = d_future_matrix->columnCount();
    for (int i = 0; i < d_future_matrix->rowCount(); i++) {
        out_text += QString::number(i) + "\t";
        for (int j = 0; j < cols - 1; j++)
            out_text += QString::number(cell(i, j), 'e', 16) + "\t";

        out_text += QString::number(cell(i, cols - 1), 'e', 16) + "\n";
    }
    return out_text + "</data>\n";
}

void Matrix::setFormula(const QString &s)
{
    d_future_matrix->setFormula(s);
}

QString Matrix::formula()
{
    return d_future_matrix->formula();
}

void Matrix::setNumericFormat(const QChar &f, int prec)
{
    if (d_future_matrix->numericFormat() == f && d_future_matrix->displayedDigits() == prec)
        return;

    d_future_matrix->setNumericFormat(f.toLatin1());
    d_future_matrix->setDisplayedDigits(prec);

    emit modifiedWindow(this);
}

void Matrix::setTextFormat(const QChar &format, int precision)
{
    d_future_matrix->setNumericFormat(format.toLatin1());
    d_future_matrix->setDisplayedDigits(precision);
}

void Matrix::setColumnsWidth(int width)
{
    for (int i = 0; i < d_future_matrix->columnCount(); i++)
        setColumnWidth(i, width);
}

void Matrix::setDimensions(int rows, int cols)
{
    d_future_matrix->setDimensions(rows, cols);
}

int Matrix::numRows()
{
    return d_future_matrix->rowCount();
}

void Matrix::setNumRows(int rows)
{
    d_future_matrix->setDimensions(rows, d_future_matrix->columnCount());
}

int Matrix::numCols()
{
    return d_future_matrix->columnCount();
}

void Matrix::setNumCols(int cols)
{
    d_future_matrix->setDimensions(d_future_matrix->rowCount(), cols);
}

double Matrix::determinant()
{
    int rows = numRows();
    int cols = numCols();

    if (rows != cols) {
        QMessageBox::critical(0, tr("Error"), tr("Calculation failed, the matrix is not square!"));
        return GSL_POSINF;
    }

    QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));

    gsl_matrix *A = gsl_matrix_alloc(rows, cols);
    int i;
    for (i = 0; i < rows; i++)
        for (int j = 0; j < cols; j++)
            gsl_matrix_set(A, i, j, cell(i, j));

    gsl_permutation *p = gsl_permutation_alloc(rows);
    gsl_linalg_LU_decomp(A, p, &i);

    double det = gsl_linalg_LU_det(A, i);

    gsl_matrix_free(A);
    gsl_permutation_free(p);

    QApplication::restoreOverrideCursor();
    return det;
}

void Matrix::invert()
{
    int rows = numRows();
    int cols = numCols();

    if (rows != cols) {
        QMessageBox::critical(0, tr("Error"), tr("Inversion failed, the matrix is not square!"));
        return;
    }

    QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));

    gsl_matrix *A = gsl_matrix_alloc(rows, cols);
    int i;
    for (i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++)
            gsl_matrix_set(A, i, j, cell(i, j));
    }

    gsl_permutation *p = gsl_permutation_alloc(cols);
    gsl_linalg_LU_decomp(A, p, &i);

    gsl_matrix *inverse = gsl_matrix_alloc(rows, cols);
    gsl_linalg_LU_invert(A, p, inverse);

    gsl_matrix_free(A);
    gsl_permutation_free(p);

    this->blockSignals(true);
    for (i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++)
            setCell(i, j, gsl_matrix_get(inverse, i, j));
    }
    this->blockSignals(false);

    gsl_matrix_free(inverse);
    QApplication::restoreOverrideCursor();
    emit modifiedWindow(this);
}

void Matrix::transpose()
{
    d_future_matrix->transpose();
}

void Matrix::saveCellsToMemory()
{
    int rows = numRows();
    int cols = numCols();
    dMatrix = allocateMatrixData(rows, cols);
    for (int i = 0; i < rows; i++)
        for (int j = 0; j < cols; j++)
            dMatrix[i][j] = d_future_matrix->cell(i, j);
}

void Matrix::forgetSavedCells()
{
    freeMatrixData(dMatrix, numRows());
    dMatrix = 0;
}

bool Matrix::recalculate()
{
    QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
    Script *script = scriptEnv->newScript(formula(), this, QString("<%1>").arg(name()));
    connect(script, SIGNAL(error(const QString &, const QString &, int)), scriptEnv,
            SIGNAL(error(const QString &, const QString &, int)));
    connect(script, SIGNAL(print(const QString &)), scriptEnv, SIGNAL(print(const QString &)));
    if (!script->compile()) {
        QApplication::restoreOverrideCursor();
        return false;
    }

    this->blockSignals(true);

    int startRow = firstSelectedRow(false);
    int endRow = lastSelectedRow(false);
    int startCol = firstSelectedColumn(false);
    int endCol = lastSelectedColumn(false);

    QVariant ret;
    saveCellsToMemory();
    double dx = fabs(xEnd() - xStart()) / (double)(numRows() - 1);
    double dy = fabs(yEnd() - yStart()) / (double)(numCols() - 1);
    std::pair<double, bool> unsetValue(std::numeric_limits<double>::quiet_NaN(), false);
    std::vector<std::vector<std::pair<double, bool>>> calculatedValues(
            endRow - startRow + 1,
            std::vector<std::pair<double, bool>>(endCol - startCol + 1, unsetValue));
    for (int row = startRow; row <= endRow; row++)
        for (int col = startCol; col <= endCol; col++) {
            if (!isCellSelected(row, col))
                continue;
            script->setInt(row + 1, "i");
            script->setInt(row + 1, "row");
            script->setDouble(yStart() + row * dy, "y");
            script->setInt(col + 1, "j");
            script->setInt(col + 1, "col");
            script->setDouble(xStart() + col * dx, "x");
            ret = script->eval();
            if (!ret.isValid()) {
                forgetSavedCells();
                blockSignals(false);
                emit modifiedWindow(this);
                QApplication::restoreOverrideCursor();
                return false;
            }
            calculatedValues[row - startRow][col - startCol] = std::make_pair(ret.toDouble(), true);
        }
    d_future_matrix->setCells(startRow, startCol, calculatedValues);
    forgetSavedCells();

    blockSignals(false);
    emit modifiedWindow(this);
    QApplication::restoreOverrideCursor();
    return true;
}

void Matrix::clearSelection()
{
    d_future_matrix->clearSelectedCells();
}

void Matrix::copySelection()
{
    d_future_matrix->copySelection();
}

void Matrix::cutSelection()
{
    copySelection();
    clearSelection();
}

bool Matrix::rowsSelected()
{
    for (int i = 0; i < numRows(); i++) {
        if (this->isRowSelected(i, true))
            return true;
    }
    return false;
}

void Matrix::deleteSelectedRows()
{
    d_future_matrix->removeSelectedRows();
}

void Matrix::insertColumn()
{
    d_future_matrix->insertEmptyColumns();
}

bool Matrix::columnsSelected()
{
    for (int i = 0; i < numCols(); i++) {
        if (this->isColumnSelected(i, true))
            return true;
    }
    return false;
}

void Matrix::deleteSelectedColumns()
{
    d_future_matrix->removeSelectedColumns();
}

int Matrix::numSelectedRows()
{
    int r = 0;
    for (int i = 0; i < numRows(); i++)
        if (this->isRowSelected(i, true))
            r++;
    return r;
}

int Matrix::numSelectedColumns()
{
    int c = 0;
    for (int i = 0; i < numCols(); i++)
        if (this->isColumnSelected(i, true))
            c++;
    return c;
}

void Matrix::insertRow()
{
    d_future_matrix->insertEmptyRows();
}

void Matrix::pasteSelection()
{
    d_future_matrix->pasteIntoSelection();
}

void Matrix::customEvent(QEvent *e)
{
    if (e->type() == SCRIPTING_CHANGE_EVENT)
        scriptingChangeEvent((ScriptingChangeEvent *)e);
}

void Matrix::closeEvent(QCloseEvent *e)
{
    if (askOnClose) {
        switch (QMessageBox::information(this, tr("SciDAVis"),
                                         tr("Do you want to hide or delete") + "<p><b>'"
                                                 + objectName() + "'</b> ?",
                                         tr("Delete"), tr("Hide"), tr("Cancel"), 0, 2)) {
        case 0:
            e->accept();
            d_future_matrix->remove();
            return;

        case 1:
            e->ignore();
            emit hiddenWindow(this);
            break;

        case 2:
            e->ignore();
            break;
        }
    } else {
        e->accept();
        d_future_matrix->remove();
        return;
    }
}

void Matrix::exportPDF(const QString &fileName)
{
    print(fileName);
}

void Matrix::print()
{
    print(QString());
}

void Matrix::print(const QString &fileName)
{
    QPrinter printer;
    printer.setColorMode(QPrinter::GrayScale);

    if (!fileName.isEmpty()) {
        printer.setCreator("SciDAVis");
        printer.setOutputFormat(QPrinter::PdfFormat);
        printer.setOutputFileName(fileName);
    } else {
        QPrintDialog printDialog(&printer);
        if (printDialog.exec() != QDialog::Accepted)
            return;
    }

    printer.setFullPage(true);
    QPainter p;
    if (!p.begin(&printer))
        return; // paint on printer
    int dpiy = printer.logicalDpiY();
    const int margin = (int)((1 / 2.54) * dpiy); // 1 cm margins

    QHeaderView *vHeader = d_view_widget->verticalHeader();

    int rows = numRows();
    int cols = numCols();
    int height = margin;
    int i, vertHeaderWidth = vHeader->width();
    int right = margin + vertHeaderWidth;

    // print header
    p.setFont(QFont());
    QString header_label = d_view_widget->model()->headerData(0, Qt::Horizontal).toString();
    QRect br = p.boundingRect(br, Qt::AlignCenter, header_label);
    p.drawLine(right, height, right, height + br.height());
    QRect tr(br);

    for (i = 0; i < cols; i++) {
        int w = columnWidth(i);
        tr.setTopLeft(QPoint(right, height));
        tr.setWidth(w);
        tr.setHeight(br.height());
        header_label = d_view_widget->model()->headerData(i, Qt::Horizontal).toString();
        p.drawText(tr, Qt::AlignCenter, header_label);
        right += w;
        p.drawLine(right, height, right, height + tr.height());

        if (right >= printer.width() - 2 * margin)
            break;
    }

    p.drawLine(margin + vertHeaderWidth, height, right - 1, height); // first horizontal line
    height += tr.height();
    p.drawLine(margin, height, right - 1, height);

    // print table values
    for (i = 0; i < rows; i++) {
        right = margin;
        QString cell_text = d_view_widget->model()->headerData(i, Qt::Vertical).toString() + "\t";
        tr = p.boundingRect(tr, Qt::AlignCenter, cell_text);
        p.drawLine(right, height, right, height + tr.height());

        br.setTopLeft(QPoint(right, height));
        br.setWidth(vertHeaderWidth);
        br.setHeight(tr.height());
        p.drawText(br, Qt::AlignCenter, cell_text);
        right += vertHeaderWidth;
        p.drawLine(right, height, right, height + tr.height());

        for (int j = 0; j < cols; j++) {
            int w = columnWidth(j);
            cell_text = text(i, j) + "\t";
            tr = p.boundingRect(tr, Qt::AlignCenter, cell_text);
            br.setTopLeft(QPoint(right, height));
            br.setWidth(w);
            br.setHeight(tr.height());
            p.drawText(br, Qt::AlignCenter, cell_text);
            right += w;
            p.drawLine(right, height, right, height + tr.height());

            if (right >= printer.width() - 2 * margin)
                break;
        }
        height += br.height();
        p.drawLine(margin, height, right - 1, height);

        if (height >= printer.height() - margin) {
            printer.newPage();
            height = margin;
            p.drawLine(margin, height, right, height);
        }
    }
}

void Matrix::range(double *min, double *max)
{
    double d_min = cell(0, 0);
    double d_max = d_min;

    for (int i = 0; i < numRows(); i++) {
        for (int j = 0; j < numCols(); j++) {
            double aux = cell(i, j);
            if (aux <= d_min)
                d_min = aux;

            if (aux >= d_max)
                d_max = aux;
        }
    }

    *min = d_min;
    *max = d_max;
}

double **Matrix::allocateMatrixData(int rows, int columns)
{
    double **data = new double *[rows];
    for (int i = 0; i < rows; ++i)
        data[i] = new double[columns];

    return data;
}

void Matrix::freeMatrixData(double **data, int rows)
{
    for (int i = 0; i < rows; i++)
        delete[] data[i];

    delete[] data;
}

void Matrix::updateDecimalSeparators()
{
    this->update();
}

void Matrix::copy(Matrix *m)
{
    if (!m)
        return;

    d_future_matrix->copy(m->d_future_matrix);
}

Matrix *Matrix::fromImage(const QImage &image, ScriptingEnv *env)
{
    future::Matrix *fm = future::Matrix::fromImage(image);
    if (!fm)
        return NULL;
    return new Matrix(fm, env, image.height(), image.width(), tr("Matrix %1").arg(1));
}

void Matrix::applyFormula()
{
    QApplication::setOverrideCursor(Qt::WaitCursor);
    d_future_matrix->beginMacro(tr("%1: apply formula to selection").arg(name()));

    setFormula(ui.formula_box->toPlainText());
    recalculate();

    d_future_matrix->endMacro();
    QApplication::restoreOverrideCursor();
}

void Matrix::addFunction()
{
    static_cast<ScriptEdit *>(ui.formula_box)
            ->insertFunction(ui.add_function_combobox->currentText());
}

void Matrix::addCell()
{
    ui.formula_box->insertPlainText(ui.add_cell_combobox->currentText());
}

void Matrix::updateFunctionDoc()
{
    ui.add_function_combobox->setToolTip(
            scriptEnv->mathFunctionDoc(ui.add_function_combobox->currentText()));
}

void Matrix::handleAspectDescriptionChange(const AbstractAspect *aspect)
{
    if (aspect != d_future_matrix)
        return;
    setObjectName(d_future_matrix->name());
    updateCaption();
}
