/*
  Copyright (c) 2009 Tobias Koenig <tokoe@kde.org>

  This library 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 library 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 library; see the file COPYING.LIB.  If not, write to the
  Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
  02110-1301, USA.
*/

#include "qcsvreader.h"

#include <QStringList>
#include <QTextCodec>
#include <QTextStream>

#include <KLocalizedString>

QCsvBuilderInterface::~QCsvBuilderInterface()
{
}

class QCsvReader::Private
{
public:
    Private(QCsvBuilderInterface *builder)
        : mBuilder(builder)
        , mNotTerminated(true)
    {
        mTextQuote = QLatin1Char('"');
        mDelimiter = QLatin1Char(' ');
        mStartRow = 0;
        mCodec = QTextCodec::codecForLocale();
    }

    void emitBeginLine(uint row);
    void emitEndLine(uint row);
    void emitField(const QString &data, int row, int column);

    QCsvBuilderInterface *mBuilder = nullptr;
    QTextCodec *mCodec = nullptr;
    QChar mTextQuote;
    QChar mDelimiter;

    uint mStartRow = 0;
    bool mNotTerminated = true;
};

void QCsvReader::Private::emitBeginLine(uint row)
{
    if ((row - mStartRow) > 0) {
        mBuilder->beginLine();
    }
}

void QCsvReader::Private::emitEndLine(uint row)
{
    if ((row - mStartRow) > 0) {
        mBuilder->endLine();
    }
}

void QCsvReader::Private::emitField(const QString &data, int row, int column)
{
    if ((row - mStartRow) > 0) {
        mBuilder->field(data, row - mStartRow - 1, column - 1);
    }
}

QCsvReader::QCsvReader(QCsvBuilderInterface *builder)
    : d(new Private(builder))
{
    Q_ASSERT(builder);
}

QCsvReader::~QCsvReader()
{
    delete d;
}

bool QCsvReader::read(QIODevice *device)
{
    enum State {
        StartLine,
        QuotedField,
        QuotedFieldEnd,
        NormalField,
        EmptyField
    };

    int row, column;

    QString field;
    QChar input;
    State currentState = StartLine;

    row = column = 1;

    d->mBuilder->begin();

    if (!device->isOpen()) {
        d->emitBeginLine(row);
        d->mBuilder->error(i18n("Device is not open"));
        d->emitEndLine(row);
        d->mBuilder->end();
        return false;
    }

    QTextStream inputStream(device);
    inputStream.setCodec(d->mCodec);

    /**
    * We use the following state machine to parse CSV:
    *
    * digraph {
    *   StartLine -> StartLine [label="\\r\\n"]
    *   StartLine -> QuotedField [label="Quote"]
    *   StartLine -> EmptyField [label="Delimiter"]
    *   StartLine -> NormalField [label="Other Char"]
    *
    *   QuotedField -> QuotedField [label="\\r\\n"]
    *   QuotedField -> QuotedFieldEnd [label="Quote"]
    *   QuotedField -> QuotedField [label="Delimiter"]
    *   QuotedField -> QuotedField [label="Other Char"]
    *
    *   QuotedFieldEnd -> StartLine [label="\\r\\n"]
    *   QuotedFieldEnd -> QuotedField [label="Quote"]
    *   QuotedFieldEnd -> EmptyField [label="Delimiter"]
    *   QuotedFieldEnd -> EmptyField [label="Other Char"]
    *
    *   EmptyField -> StartLine [label="\\r\\n"]
    *   EmptyField -> QuotedField [label="Quote"]
    *   EmptyField -> EmptyField [label="Delimiter"]
    *   EmptyField -> NormalField [label="Other Char"]
    *
    *   NormalField -> StartLine [label="\\r\\n"]
    *   NormalField -> NormalField [label="Quote"]
    *   NormalField -> EmptyField [label="Delimiter"]
    *   NormalField -> NormalField [label="Other Char"]
    * }
    */

    while (!inputStream.atEnd() && d->mNotTerminated) {
        inputStream >> input;

        switch (currentState) {
        case StartLine:
            if (input == QLatin1Char('\r') || input == QLatin1Char('\n')) {
                currentState = StartLine;
            } else if (input == d->mTextQuote) {
                d->emitBeginLine(row);
                currentState = QuotedField;
            } else if (input == d->mDelimiter) {
                d->emitBeginLine(row);
                d->emitField(field, row, column);
                column++;
                currentState = EmptyField;
            } else {
                d->emitBeginLine(row);
                field.append(input);
                currentState = NormalField;
            }
            break;
        case QuotedField:
            if (input == QLatin1Char('\r') || input == QLatin1Char('\n')) {
                field.append(input);
                currentState = QuotedField;
            } else if (input == d->mTextQuote) {
                currentState = QuotedFieldEnd;
            } else if (input == d->mDelimiter) {
                field.append(input);
                currentState = QuotedField;
            } else {
                field.append(input);
                currentState = QuotedField;
            }
            break;
        case QuotedFieldEnd:
            if (input == QLatin1Char('\r') || input == QLatin1Char('\n')) {
                d->emitField(field, row, column);
                field.clear();
                d->emitEndLine(row);
                column = 1;
                row++;
                currentState = StartLine;
            } else if (input == d->mTextQuote) {
                field.append(input);
                currentState = QuotedField;
            } else if (input == d->mDelimiter) {
                d->emitField(field, row, column);
                field.clear();
                column++;
                currentState = EmptyField;
            } else {
                d->emitField(field, row, column);
                field.clear();
                column++;
                field.append(input);
                currentState = EmptyField;
            }
            break;
        case NormalField:
            if (input == QLatin1Char('\r') || input == QLatin1Char('\n')) {
                d->emitField(field, row, column);
                field.clear();
                d->emitEndLine(row);
                row++;
                column = 1;
                currentState = StartLine;
            } else if (input == d->mTextQuote) {
                field.append(input);
                currentState = NormalField;
            } else if (input == d->mDelimiter) {
                d->emitField(field, row, column);
                field.clear();
                column++;
                currentState = EmptyField;
            } else {
                field.append(input);
                currentState = NormalField;
            }
            break;
        case EmptyField:
            if (input == QLatin1Char('\r') || input == QLatin1Char('\n')) {
                d->emitField(QString(), row, column);
                field.clear();
                d->emitEndLine(row);
                column = 1;
                row++;
                currentState = StartLine;
            } else if (input == d->mTextQuote) {
                currentState = QuotedField;
            } else if (input == d->mDelimiter) {
                d->emitField(QString(), row, column);
                column++;
                currentState = EmptyField;
            } else {
                field.append(input);
                currentState = NormalField;
            }
            break;
        }
    }

    if (currentState != StartLine) {
        if (field.length() > 0) {
            d->emitField(field, row, column);
            ++row;
            field.clear();
        }
        d->emitEndLine(row);
    }

    d->mBuilder->end();

    return true;
}

void QCsvReader::setTextQuote(QChar textQuote)
{
    d->mTextQuote = textQuote;
}

QChar QCsvReader::textQuote() const
{
    return d->mTextQuote;
}

void QCsvReader::setDelimiter(QChar delimiter)
{
    d->mDelimiter = delimiter;
}

QChar QCsvReader::delimiter() const
{
    return d->mDelimiter;
}

void QCsvReader::setStartRow(uint startRow)
{
    d->mStartRow = startRow;
}

uint QCsvReader::startRow() const
{
    return d->mStartRow;
}

void QCsvReader::setTextCodec(QTextCodec *textCodec)
{
    d->mCodec = textCodec;
}

QTextCodec *QCsvReader::textCodec() const
{
    return d->mCodec;
}

void QCsvReader::terminate()
{
    d->mNotTerminated = false;
}

class QCsvStandardBuilder::Private
{
public:
    Private()
    {
        init();
    }

    void init();

    QString mLastErrorString;
    uint mRowCount;
    uint mColumnCount;
    QList<QStringList> mRows;
};

void QCsvStandardBuilder::Private::init()
{
    mRows.clear();
    mRowCount = 0;
    mColumnCount = 0;
    mLastErrorString.clear();
}

QCsvStandardBuilder::QCsvStandardBuilder()
    : d(new Private)
{
}

QCsvStandardBuilder::~QCsvStandardBuilder()
{
    delete d;
}

QString QCsvStandardBuilder::lastErrorString() const
{
    return d->mLastErrorString;
}

uint QCsvStandardBuilder::rowCount() const
{
    return d->mRowCount;
}

uint QCsvStandardBuilder::columnCount() const
{
    return d->mColumnCount;
}

QString QCsvStandardBuilder::data(uint row, uint column) const
{
    if (row > d->mRowCount || column > d->mColumnCount || column >= (uint)d->mRows[ row ].count()) {
        return QString();
    }

    return d->mRows[ row ][ column ];
}

void QCsvStandardBuilder::begin()
{
    d->init();
}

void QCsvStandardBuilder::beginLine()
{
    d->mRows.append(QStringList());
    d->mRowCount++;
}

void QCsvStandardBuilder::field(const QString &data, uint row, uint column)
{
    const uint size = d->mRows[ row ].size();
    if (column >= size) {
        for (uint i = column; i < size + 1; ++i) {
            d->mRows[ row ].append(QString());
        }
    }

    d->mRows[ row ][ column ] = data;

    d->mColumnCount = qMax(d->mColumnCount, column + 1);
}

void QCsvStandardBuilder::endLine()
{
}

void QCsvStandardBuilder::end()
{
}

void QCsvStandardBuilder::error(const QString &errorMsg)
{
    d->mLastErrorString = errorMsg;
}
