/*
 * This file is part of KSpread
 *
 * Copyright (c) 2007 Sebastian Sauer <mail@dipe.org>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Library 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 Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library 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.
 */

#ifndef SCRIPTINGREADER_H
#define SCRIPTINGREADER_H

#include <QString>
#include <QStringList>
#include <QObject>

#include "ScriptingModule.h"

#include <part/Doc.h>
#include <Sheet.h>
#include <Map.h>
#include <Region.h>
#include <Cell.h>
#include <Value.h>

/**
* The ScriptingReader class provides abstract high-level functionality to read
* content from KSpread sheets.
*
* The following python sample demonstrates how to use the ScriptingReader to
* read content from KSpread.
* \code
* # Import the KSpread module
* import KSpread
*
* # Create a ScriptingReader object
* reader = KSpread.reader()
* # We like to read all of Sheet1
* reader.setSheet("Sheet1")
* # and from Sheet2 only the range A5:F12
* reader.setSheet("Sheet2","A5:F12")
*
* # This function got called in our case two times.
* # One time if we switch to Sheet1 and the other
* # time if switched to Sheet2.
* def changedSheet(sheetname):
*     print "sheetname=%s" % sheetname
*
* # This function got called for each row that got
* # readed.
* def changedRow(row):
*     values = reader.currentValues()
*     print "row=%i values=%s" % (row,values)
*
* # Now let's connect our both functions to matching
* # signals the reader provides.
* reader.connect("changedSheet(QString)",changedSheet)
* reader.connect("changedRow(int)",changedRow)
* # and finally start the reader.
* reader.start()
* \endcode
*/
class ScriptingReader : public QObject
{
    Q_OBJECT
public:

    /**
    * Constructor.
    * \param module The \a ScriptingModule instance that should be used
    * as datasource for this reader.
    */
    explicit ScriptingReader(ScriptingModule* module) : QObject(module), m_module(module) {
        clear();
    }

    /**
    * Destructor.
    */
    ~ScriptingReader() override {}

public Q_SLOTS:

    /**
    * Start the reading. This let's the reader run over the defined sheets and there ranges
    * and emits the signals this reader provides while walking over the data.
    */
    bool start() {
        if (m_state != Stopped) return false;
        m_state = Running;
        QStringList sheets = m_sheetnames.count() > 0 ? m_sheetnames : m_module->sheetNames();
        foreach(QString sheetname, sheets) {
            readSheet(sheetname);
            if (m_state != Running) break;
        }
        clear();
        return true;
    }

    /**
    * Stop the reading. This method does request to shutdown a reading process what means,
    * that reading will stop as soon as possible. This does make it save to call this
    * method also from within scripting functions that connected with the signals this
    * reader provides.
    */
    void stop() {
        if (m_state == Running)
            m_state = Shutdown;
    }

    /**
    * \returns true if the reader is still running what includes also shutdown. If the
    * reader is stopped and can be started again by using the \a start() method false
    * got returned.
    */
    bool isRunning() const {
        return m_state != Stopped;
    }

    /**
    * \return the names of the sheets that should be readed. If the list is empty and the
    * reader got started, then the reader will walk through all sheets the document has.
    */
    QStringList sheetNames() const {
        return m_sheetnames;
    }

    /**
    * \return the defined ranges for the \p sheetname . If there was no range defined,
    * then we walk over all the data the sheet has.
    */
    QVariantList range(const QString& sheetname) const {
        return m_ranges.contains(sheetname) ? m_ranges[sheetname] : QVariantList();
    }

    /**
    * Set the defined ranges for the \p sheetname to \p range .
    */
    void setRange(const QString& sheetname, const QVariantList& range) {
        m_ranges[sheetname] = range;
    }

    /**
    * Set the names of the sheets that should be readed to \p sheetnames .
    */
    void setSheetNames(const QStringList& sheetnames) {
        m_sheetnames = sheetnames;
        m_ranges.clear();
    }

    /**
    * Set the sheets and there ranges.
    *
    * For example in python following structure is a valid definition
    * for 3 sheets where Sheet1 and Sheet2 are selected. Sheet1 also
    * does define the range A1:B2.
    * [['Sheet1', 1, [1, 1, 2, 2]], ['Sheet2', 1], ['Sheet3', 0]]
    *
    * Each sheet contains a tuple of
    * \li sheetname
    * \li 1=enabled or 0=disabled
    * \li optional range tuple [from column, from row, to column, to row]
    */
    void setSheets(const QVariantList& sheets = QVariantList()) {
        m_sheetnames.clear();
        m_ranges.clear();
        foreach(QVariant item, sheets) {
            QList<QVariant> args = item.toList();
            if (args.count() < 2) continue;
            if (! args[1].toBool()) continue;
            const QString sheetname = args[0].toString();
            if (sheetname.isEmpty()) continue;
            m_sheetnames.append(sheetname);
            if (args.count() > 2) {
                args.removeFirst();
                args.removeFirst();
                m_ranges[sheetname] = args;
            }
        }
    }

    /**
    * Set a sheet that should be readed. The sheet with name \p sheetname and
    * the optional defined range \p range will be appended to all already
    * defined sheets if no such sheet with \p sheetname was defined before else
    * we just replace the previous definition including the range.
    * The range should looks like e.g. "A1:C3" while the defined sheetname
    * should be an existing one if a range got defined.
    */
    void setSheet(const QString& sheetname, const QString& range = QString()) {
        if (! m_sheetnames.contains(sheetname))
            m_sheetnames.append(sheetname);
        if (range.isNull()) {
            m_ranges.remove(sheetname);
            return;
        }
        Calligra::Sheets::Sheet* sheet = m_module->kspreadDoc()->map()->findSheet(sheetname);
        if (! sheet) return;
        QVariantList ranges;
        Calligra::Sheets::Region region(range, m_module->kspreadDoc()->map(), sheet);
        if (! region.isValid()) return;
        for (Calligra::Sheets::Region::ConstIterator it = region.constBegin(); it != region.constEnd(); ++it) {
            const QRect rect = (*it)->rect();
            if (rect.isNull()) continue;
            ranges.append(rect);
        }
        m_ranges[sheetname] = ranges;
    }

    /**
    * \return the sheetname the reader currently is on. An empty/null string will be
    * returned if there is no current sheet (e.g. if the reader just doesn't run currently).
    */
    QString currentSheetName() const {
        return m_currentSheet ? m_currentSheet->sheetName() : QString();
    }

    /**
    * \return the current row number the reader is on. This will be -1 if the reader
    * isn't running.
    */
    int currentRow() const {
        return m_currentRow;
    }

    /**
    * \return the most left column the current row has or -1 if there is no current row.
    */
    int currentFirstColumn() const {
        return m_currentLeft;
    }

    /**
    * \return the most right column the current row has or -1 if there is no current row.
    */
    int currentLastColumn() const {
        return m_currentRight;
    }

    /**
    * \return a list of values for the current row. This will be an empty list of the
    * reader isn't running.
    */
    QVariantList currentValues() const {
        QVariantList values;
        if (m_currentSheet && m_currentRow >= 0) {
            for (int col = m_currentLeft; col <= m_currentRight; ++col) {
                Calligra::Sheets::Cell cell(m_currentSheet, col, m_currentRow);
                Calligra::Sheets::Value value = cell.value();

                //TODO add toVariant() method to Calligra::Sheets::Value and use it here and in SheetAdaptor::valueToVariant
                //values << value.toVariant();

                QVariant v;
                switch (value.type()) {
                case Calligra::Sheets::Value::Empty: break;
                case Calligra::Sheets::Value::Boolean: v = value.asBoolean(); break;
                case Calligra::Sheets::Value::Integer: v = value.asInteger(); break;
                case Calligra::Sheets::Value::Float: v = (double) numToDouble(value.asFloat()); break;
                case Calligra::Sheets::Value::String: //fall through
                default: v = value.asString(); break;
                }
                values << v;
            }
        }
        return values;
    }


Q_SIGNALS:

    /**
    * This signal is emitted once the reading started with the \a start() method
    * changed to the sheet with name \p sheetname cause e.g. reading the previous
    * sheet was done.
    */
    void changedSheet(const QString& sheetname);

    /**
    * This signal is emitted once the reading started with the \a start() method
    * changed to the row \p rownumber where rownumber is >=0.
    */
    void changedRow(int rownumber);

private:
    Q_DISABLE_COPY(ScriptingReader)

    ScriptingModule* const m_module;
    QStringList m_sheetnames;
    QHash<QString, QVariantList> m_ranges;

    enum State { Stopped, Running, Shutdown };
    State m_state;
    Calligra::Sheets::Sheet* m_currentSheet;
    int m_currentRow, m_currentLeft, m_currentRight;

    void clear() {
        m_state = Stopped;
        m_currentSheet = 0;
        m_currentRow = m_currentLeft = m_currentRight = -1;
    }

    void readSheet(const QString& sheetname) {
        m_currentSheet = m_module->kspreadDoc()->map()->findSheet(sheetname);
        if (! m_currentSheet) return;
        emit changedSheet(sheetname);
        if (m_state != Running) return;
        //debugSheetsScripting<<"ScriptingReader::readSheet sheetname="<<sheetname;
        QVariantList range = m_ranges[sheetname];
        if (range.count() > 0) {
            foreach(QVariant r, range) {
                QVariantList l = r.toList();
                if (l.count() < 4) continue;
                QRect rect(l[0].toInt(), l[1].toInt(), l[2].toInt(), l[3].toInt());
                if (rect.isNull() || (rect.x() == 0 && rect.y() == 0 && rect.width() == 0 && rect.height() == 0)) continue;
                //debugSheetsScripting<<"  string="<<r.toString()<<" rect="<<rect;
                Calligra::Sheets::Region region(rect, m_currentSheet);
                readRegion(region);
                if (m_state != Running) break;
            }
        } else {
            QRect area = m_currentSheet->usedArea();
            if (area.isNull()) return;
            Calligra::Sheets::Region region(area, m_currentSheet);
            readRegion(region);
        }
    }

    void readRegion(const Calligra::Sheets::Region& region) {
        if (! m_currentSheet || ! region.isValid()) return;
        //debugSheetsScripting<<"ScriptingReader::readRegion name="<<region.name(m_currentSheet);
        for (Calligra::Sheets::Region::ConstIterator it = region.constBegin(); it != region.constEnd(); ++it) {
            QRect range = (*it)->rect();
            if (range.isNull()) continue;
            //debugSheetsScripting <<"  name=" << (*it)->name(m_currentSheet) <<" range=" << range;
            const int bottom = range.bottom();
            m_currentLeft = range.left();
            m_currentRight = range.right();
            for (int row = range.top(); row <= bottom; ++row) {
                m_currentRow = row;
                emit changedRow(row);
                if (m_state != Running) break;
            }
        }
    }
};

#endif
