/*
  SPDX-FileCopyrightText: 2009 Tobias Koenig <tokoe@kde.org>

  SPDX-License-Identifier: LGPL-2.0-or-later
*/

#include "qcsvreader.h"

#include <KLocalizedString>
#include <QStringList>
#include <QTextCodec>
#include <QTextStream>
#include <QVector>

QCsvBuilderInterface::~QCsvBuilderInterface() = default;

class QCsvReaderPrivate
{
public:
    explicit QCsvReaderPrivate(QCsvBuilderInterface *builder)
        : mBuilder(builder)
        , mCodec(QTextCodec::codecForLocale())
    {
    }

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

    QCsvBuilderInterface *const mBuilder;
    QTextCodec *mCodec = nullptr;
    QChar mTextQuote = QLatin1Char('"');
    QChar mDelimiter = QLatin1Char(' ');

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

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

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

void QCsvReaderPrivate::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 QCsvReaderPrivate(builder))
{
    Q_ASSERT(builder);
}

QCsvReader::~QCsvReader() = default;

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

    int row;
    int 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 QCsvStandardBuilderPrivate
{
public:
    QCsvStandardBuilderPrivate()
    {
        init();
    }

    void init();

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

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

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

QCsvStandardBuilder::~QCsvStandardBuilder() = default;

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 {};
    }

    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;
}
