/* This file is part of the KDE project

   SPDX-FileCopyrightText: 2008 Johannes Simon <johannes.simon@gmail.com>
   SPDX-FileCopyrightText: 2008 Inge Wallin <inge@lysator.liu.se>

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

// Own
#include "CellRegion.h"

// C
#include <cmath>

// Qt
#include <QPoint>
#include <QRect>
#include <QStringList>
#include <QVector>

// KoChart
#include "ChartDebug.h"
#include "TableSource.h"

using std::pow;
using namespace KoChart;

/************************RegionParser*******************************/

class Parser
{
public:
    Parser(const QString &input)
        : m_pos(m_input.constEnd())
    {
        m_input = input;
        if (m_input.contains(":.")) {
            // FIXME
            warnChartParse << "Handle 'DotDoubleDot' in input string";
            m_input.replace(QStringLiteral(":."), QChar(':'));
        }
        m_delimiter.append(QChar::fromLatin1('.'));
        m_delimiter.append(QChar::fromLatin1(':'));
        m_delimiter.append(QChar::fromLatin1(';'));
        m_delimiter.append(QChar::fromLatin1(' '));
    }
    bool parse();
    QList<QRect> getResult() const
    {
        return m_result;
    }
    QString tableName() const
    {
        return m_tableName;
    }

private:
    struct Token {
        enum TokenType {
            Dot = 0,
            DoubleDot = 1,
            Space = 2,
            Spacer = 3,
            Identifier = 4,
            End
        };
        Token(TokenType type, const QString &identifier)
            : m_type(type)
            , m_identifier(identifier)
        {
        }
        Token()
            : m_type(End)
        {
        }
        TokenType m_type;
        QString m_identifier;
    };

    inline Token parseToken();
    inline void eatWhitespaces();
    inline bool parseRegionList();
    inline bool parseRegion();
    inline bool parseName();
    inline bool parseName2();
    inline bool parsePoint();
    inline bool parseRegion2();
    inline void setTableName(const QString &name);

public:
    QString toString(Token &token)
    {
        QString types = "Dot,DoubleDot,Space,Spacer,Identifier,End";
        QString s = QString("Token[%1").arg(types.split(',').value(token.m_type));
        if (token.m_type == Token::Identifier) {
            s += ", " + token.m_identifier;
        }
        s += ']';
        return s;
    }

private:
    QString m_input;
    QString::ConstIterator m_pos;
    QList<QRect> m_result;
    Token m_currentToken;
    QRect m_currentRect;
    QPoint m_currentPoint;
    QString m_tableName;
    int m_index;
    QVector<QChar> m_delimiter;
};

void Parser::setTableName(const QString &name)
{
    QString strippedName = name;
    if (name.startsWith(QChar::fromLatin1('$')))
        strippedName.remove(0, 1);
    if (m_tableName.isEmpty())
        m_tableName = strippedName;
    else if (strippedName != m_tableName)
        debugChartParse << "More than one sheet referenced, this is currently not supported";
}

bool Parser::parse()
{
    debugChartParse << "Input " << m_input;
    m_pos = m_input.constBegin();
    m_index = 0;
    m_currentToken = parseToken();
    return parseRegionList();
}

Parser::Token Parser::parseToken()
{
    Token::TokenType type = Token::End;
    if (m_pos != m_input.constEnd()) {
        switch (m_delimiter.indexOf(*m_pos)) {
        case (0):
            type = Token::Dot;
            break;
        case (1):
            type = Token::DoubleDot;
            break;
        case (2):
        case (3):
            type = Token::Space;
            break;
        default:
            type = Token::Identifier;
        }
    }
    if (m_index >= m_input.size())
        type = Token::End;
    else if (*m_pos == QChar::fromLatin1('$')) {
        ++m_pos;
        ++m_index;
    }
    QString identifier;
    if (m_pos != m_input.constEnd() && *m_pos == QChar::fromLatin1('\'')) {
        ++m_pos;
        ++m_index;
        int startPos = m_index;
        for (; m_pos != m_input.constEnd() && *m_pos != QChar::fromLatin1('\''); ++m_pos, ++m_index)
            ;

        if (type == Token::Identifier)
            identifier = m_input.mid(startPos, m_index - startPos);
        if (m_pos != m_input.constEnd()) {
            ++m_pos;
            ++m_index;
        }
    } else {
        int startPos = m_index;
        for (; m_pos != m_input.constEnd() && !m_delimiter.contains(*m_pos); ++m_pos, ++m_index)
            ;
        if (m_pos != m_input.constEnd() && startPos == m_index) {
            ++m_index;
            ++m_pos;
        }
        if (type == Token::Identifier)
            identifier = m_input.mid(startPos, m_index - startPos);
    }

    Token t(type, identifier);
    debugChartParse << toString(t);
    return t;
}

void Parser::eatWhitespaces()
{
    for (; m_pos != m_input.constEnd() && *m_pos == QChar::fromLatin1(' '); ++m_pos, ++m_index)
        ;
}

bool Parser::parseRegionList()
{
    bool res = true;
    for (; m_currentToken.m_type != Token::End; m_currentToken = parseToken()) {
        if (m_currentToken.m_type != Token::Space) {
            if (m_currentToken.m_type == Token::Identifier)
                res = parseRegion();
            else
                res = false;
        }
    }
    return res;
}

bool Parser::parseRegion()
{
    debugChartParse << "parseRegion";
    bool res = true;
    res &= parseRegion2();
    m_currentToken = parseToken();
    debugChartParse << "CurrentToken " << m_currentToken.m_identifier << m_currentToken.m_type;
    if (m_currentToken.m_type == Token::DoubleDot) {
        const QPoint topLeft = m_currentPoint;
        m_currentToken = parseToken();
        res &= parseRegion2();
        // m_currentToken = parseToken();
        m_result.append(QRect(topLeft, m_currentPoint));
        debugChartParse << "DoubleDot"
                        << "result:" << m_result;
    } else {
        m_result.append(QRect(m_currentPoint, m_currentPoint));
        debugChartParse << "NoDoubleDot"
                        << "result:" << m_result;
        ;
    }

    if (m_currentToken.m_type == Token::Space) {
        res &= parseRegionList();
    } else if (m_currentToken.m_type == Token::End) {
        debugChartParse << "ParseRegion:" << res << toString(m_currentToken) << m_result;
        ;
        return res;
    } else {
        res = false;
    }
    debugChartParse << "ParseRegion:" << res << toString(m_currentToken) << m_result;
    ;
    return res;
}

bool Parser::parseRegion2()
{
    // debugChart << "ParseRegion2";
    bool res = true;

    if (m_currentToken.m_type != Token::Identifier && m_currentToken.m_type != Token::Dot)
        res = false;

    const QString firstIdentifier = m_currentToken.m_type != Token::Dot ? m_currentToken.m_identifier : tableName();
    if (m_currentToken.m_type != Token::Dot)
        m_currentToken = parseToken();
    if (m_currentToken.m_type == Token::Dot) {
        m_currentToken = parseToken();
        if (m_currentToken.m_type == Token::Identifier) {
            static QRegularExpression regEx(QString::fromLatin1("^([$]*)([A-Z]+)([$]*)([0-9]+)$"));
            auto match = regEx.match(m_currentToken.m_identifier);
            m_currentPoint = QPoint(CellRegion::rangeStringToInt(match.captured(2)), match.captured(4).toInt());
            // debugChart << "FUN" << regEx.cap(2) << " " << regEx.cap(4);
            setTableName(firstIdentifier);
        } else
            res = false;
    } else {
        QRegularExpression regEx(QString::fromLatin1("^([$]*)([A-Z]+)([$]*)([0-9]+)$"));
        auto match = regEx.match(firstIdentifier);
        // debugChart << "FUN" << regEx.cap(2) << " " << regEx.cap(4);
        m_currentPoint = QPoint(CellRegion::rangeStringToInt(match.captured(2)), match.captured(4).toInt());
    }
    // debugChart << "TableName "<< m_tableName;
    // debugChart << firstIdentifier;
    // debugChart << "Point" << m_currentPoint;
    // debugChart << m_currentToken.m_identifier;
    // debugChart << m_currentToken.m_type;

    return res;
}

// bool Parser::parsePoint()
// {
//     bool res = true;
//     int startIndex = m_index;
//     while(m_pos != m_input.end() && *m_pos != QChar::fromLatin1(':'))
//     {
//         ++m_pos;
//         ++m_index;
//     }
//     const QString currentString = m_input.mid(startIndex, m_index - startIndex);
//     debugChart << "PointString" << currentString;
//     QRegExp regEx(QString::fromLatin1("[A-Z]+[0-9]+"));
//     regEx.indexIn(currentString);
//     m_currentPoint = QPoint(CellRegion::rangeStringToInt(regEx.cap(0)), regEx.cap(1).toInt());
//     return res;
// }
/************************ENDRegionParser*******************************/

static QString columnName(uint column);
// static int rangeCharToInt(char c);

/**
 * Makes sure that quotes are added if name contains spaces or special
 * characters. May also be used to escape certain characters if needed.
 */
static QString formatTableName(const QString &name)
{
    static const QList<QChar> specialChars = QList<QChar>() << ' ' << '\t' << '-' << '\'';

    bool containsSpecialChars = false;
    foreach (QChar c, specialChars)
        containsSpecialChars = containsSpecialChars || name.contains(c);

    if (containsSpecialChars)
        return QLatin1Char('\'') + name + QLatin1Char('\'');

    return name;
}

/**
 * Reverts any operation done by formatTableName(), so that ideally
 * unformatTableName(formatTableName(name)) == name
 */
/* currently not used in CellRegion(TableSource *source, const QString& regions)
static QString unformatTableName(QString name)
{
    if (name.startsWith(QLatin1Char('\'')) && name.endsWith(QLatin1Char('\''))) {
        name.remove(0, 1).chop(1);
    }

    return name;
}
*/
class CellRegion::Private
{
public:
    Private();
    ~Private();

    QString pointToString(const QPoint &point) const;

    // These are actually one-dimensional, but can have different
    // orientations (hor / vert).
    QVector<QRect> rects;

    QRect boundingRect;
    // NOTE: Don't forget to extend operator=() if you add new members

    /// Table this region is in (name/model pair provided by TableSource)
    Table *table;
};

CellRegion::Private::Private()
{
    table = nullptr;
}

CellRegion::Private::~Private() = default;

// ================================================================
//                         Class CellRegion

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

CellRegion::CellRegion(const CellRegion &region)
    : d(new Private())
{
    // Use operator=();
    *this = region;
}

CellRegion::CellRegion(TableSource *source, const QString &regions)
    : d(new Private())
{
    // A dollar sign before a part of the address means that this part
    // is absolute. This is irrelevant for us, however, thus we can remove
    // all occurrences of '$', and handle relative and absolute addresses in
    // the same way.
    // See ODF specs $8.3.1 "Referencing Table Cells"
    Parser parser(regions);
    const bool success = parser.parse();
    if (!success)
        warnChart << "Parsing cell region failed:" << regions;

    QVector<QRect> rects = parser.getResult().toVector();
    for (int i = 0; i < rects.count(); ++i) {
        add(rects.at(i));
    }
    d->table = source->get(parser.tableName());
    //     QStringList regionsList = regions.split(' ', Qt::SkipEmptyParts);
    //     Q_FOREACH(const QString& region, regionsList) {
    //       QString searchStr = QString(region).remove('$');
    //       QRegExp regEx;
    //
    //       QStringList regionList = searchStr.split(';');
    //       Q_FOREACH(const QString &region, regionList) {
    //           const bool isPoint = !region.contains(':');
    //           if (isPoint)
    //               regEx = QRegExp("(|.*\\.)([A-Z]+)([0-9]+)");
    //           else // support range-notations like Sheet1.D2:Sheet1.F2 Sheet1.D2:F2 D2:F2
    //               regEx = QRegExp ("(|.*\\.)([A-Z]+)([0-9]+)\\:(|.*\\.)([A-Z]+)([0-9]+)");
    //
    //           // Check if region string is valid (e.g. not empty)
    //           if (regEx.indexIn(region) >= 0) {
    //               // It is possible for a cell-range-address as defined in ODF to contain
    //               // references to cells of more than one sheet. This, however, we ignore
    //               // here. We do not support more than one table in a cell region.
    //               // Also we do not support regions spanned over different sheets. For us
    //               // everything is either on no sheet or on the same sheet.
    //               QString sheetName = regEx.cap(1);
    //               if (sheetName.endsWith("."))
    //                   sheetName = sheetName.left(sheetName.length() - 1);
    //               // TODO: Support for multiple tables in one region
    //               d->table = source->get(unformatTableName(sheetName));
    //
    //               QPoint topLeft(rangeStringToInt(regEx.cap(2)), regEx.cap(3).toInt());
    //               if (isPoint) {
    //                   d->rects.append(QRect(topLeft, QSize(1, 1)));
    //               } else {
    //                   QPoint bottomRight(rangeStringToInt(regEx.cap(5)), regEx.cap(6).toInt());
    //                   d->rects.append(QRect(topLeft, bottomRight));
    //               }
    //           }
    //       }
    //     }
}

CellRegion::CellRegion(Table *table, const QPoint &point)
    : d(new Private())
{
    d->table = table;
    add(point);
}

CellRegion::CellRegion(Table *table, const QRect &rect)
    : d(new Private())
{
    d->table = table;
    add(rect);
}

CellRegion::CellRegion(Table *table, const QVector<QRect> &rects)
    : d(new Private())
{
    d->table = table;
    foreach (const QRect &rect, rects)
        add(rect);
}

CellRegion::CellRegion(Table *table)
    : d(new Private())
{
    d->table = table;
}

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

CellRegion &CellRegion::operator=(const CellRegion &region)
{
    d->rects = region.d->rects;
    d->boundingRect = region.d->boundingRect;
    d->table = region.d->table;

    return *this;
}

bool CellRegion::operator==(const CellRegion &other) const
{
    return d->rects == other.d->rects;
}

Table *CellRegion::table() const
{
    return d->table;
}

QVector<QRect> CellRegion::rects() const
{
    return d->rects;
}

int CellRegion::rectCount() const
{
    return d->rects.size();
}

QString CellRegion::sheetName() const
{
    return d->table->name();
}

bool CellRegion::isValid() const
{
    return d->rects.size() > 0 && d->table;
}

QString CellRegion::Private::pointToString(const QPoint &point) const
{
    QString result;

    result.append('$' + columnName(point.x()));
    result.append('$' + QString::number(point.y()));

    return result;
}

QString CellRegion::toString() const
{
    if (!isValid())
        return QString();

    QString result;
    for (int i = 0; i < d->rects.count(); ++i) {
        const QRect range = d->rects[i];
        // Top-left corner
        if (table())
            result.append('$' + formatTableName(table()->name()) + '.');
        result.append(d->pointToString(range.topLeft()));

        // If it is not a point, append rect's bottom-right corner
        if (range.topLeft() != range.bottomRight()) {
            result.append(':');
            result.append(d->pointToString(range.bottomRight()));
        }

        // Separate ranges by a space
        // See odf 18.3.6cellRangeAddressList
        if (i < d->rects.count() - 1)
            result.append(' ');
    }
    return result;
}

bool CellRegion::contains(const QPoint &point, bool proper) const
{
    foreach (const QRect &rect, d->rects) {
        if (rect.contains(point, proper))
            return true;
    }

    return false;
}

bool CellRegion::contains(const QRect &rect, bool proper) const
{
    foreach (const QRect &r, d->rects) {
        if (r.contains(rect, proper))
            return true;
    }

    return false;
}

bool CellRegion::intersects(const CellRegion &other) const
{
    // If both regions lie within only one table and these tables
    // are different, they trivially do not intersect.
    if (table() && other.table() && table() != other.table())
        return false;

    foreach (const QRect &r, d->rects) {
        foreach (const QRect &_r, other.d->rects) {
            if (r.intersects(_r))
                return true;
        }
    }

    return false;
}

CellRegion CellRegion::intersected(const QRect &rect) const
{
    CellRegion intersections;

    foreach (const QRect &r, d->rects) {
        if (r.intersects(rect))
            intersections.add(r.intersected(rect));
    }

    return intersections;
}

Qt::Orientation CellRegion::orientation() const
{
    foreach (const QRect &rect, d->rects) {
        if (rect.width() > 1)
            return Qt::Horizontal;
        if (rect.height() > 1)
            return Qt::Vertical;
    }

    // Default if region is only one cell
    return Qt::Vertical;
}

int CellRegion::cellCount() const
{
    int count = 0;

    /*FIXME the following would be more correct cause it
     * would also cover multi-dimensional ranges (means
     * where rect.width()>1 *and* rect.height()>1). But
     * for that kchart needs lot of fixing (e.g. in
     * the CellRegion to proper handle multi-dimension
     * ranges too).
     *
    foreach (const QRect &rect, d->rects)
        count += (rect.width() * rect.height());
    */

    if (orientation() == Qt::Horizontal) {
        foreach (const QRect &rect, d->rects)
            count += rect.width();
    } else {
        foreach (const QRect &rect, d->rects)
            count += rect.height();
    }
    return count;
}

void CellRegion::add(const CellRegion &other)
{
    add(other.rects());
}

void CellRegion::add(const QPoint &point)
{
    add(QRect(point, QSize(1, 1)));
}

void CellRegion::add(const QRect &rect)
{
// These checks are obsolete, a CellRegion can be used otherwise as well
#if 0
    if (!rect.isValid()) {
        warnChart << "CellRegion::add() Attempt to add invalid rectangle";
        warnChart << "CellRegion::add():" << rect;
        return;
    }

    if (rect.width() > 1 && rect.height() > 1) {
        warnChart << "CellRegion::add() Attempt to add rectangle with height AND width > 1";
        warnChart << "CellRegion::add():" << rect;
        return;
    }
#endif

    d->rects.append(rect);
    d->boundingRect |= rect;
}

void CellRegion::add(const QVector<QRect> &rects)
{
    foreach (const QRect &rect, rects)
        add(rect);
}

QRect CellRegion::boundingRect() const
{
    return d->boundingRect;
}

bool CellRegion::hasPointAtIndex(int index) const
{
    return pointAtIndex(index) != QPoint(-1, -1);
}

QPoint CellRegion::pointAtIndex(int index) const
{
    // sum of all previous rectangle indices
    int i = 0;

    foreach (const QRect &rect, d->rects) {
        // Rectangle is horizontal
        if (rect.width() > 1) {
            // Found it!
            // Index refers to point in current rectangle
            if (i + rect.width() > index) {
                // Local index of point in this rectangle
                int j = index - i;
                return QPoint(rect.x() + j, rect.y());
            }

            // add number of indices in current rectangle to total index count
            i += rect.width();
        } else {
            // Found it!
            // Index refers to point in current rectangle
            if (i + rect.height() > index) {
                // Local index of point in this rectangle
                int j = index - i;
                return QPoint(rect.x(), rect.y() + j);
            }

            // add number of indices in current rectangle to total index count
            i += rect.height();
        }
    }

    // Invalid index!
    return QPoint(-1, -1);
}

int CellRegion::indexAtPoint(const QPoint &point) const
{
    int indicesLeftToPoint = 0;
    bool found = false;

    foreach (const QRect &rect, d->rects) {
        if (!rect.contains(point)) {
            indicesLeftToPoint += rect.width() > 1 ? rect.width() : rect.height();
            continue;
        }

        found = true;
        if (rect.width() > 1)
            indicesLeftToPoint += point.x() - rect.topLeft().x();
        else
            indicesLeftToPoint += point.y() - rect.topLeft().y();
    }

    return found ? indicesLeftToPoint : -1;
}

#if 0 // Unused?
static int rangeCharToInt(char c)
{
    return (c >= 'A' && c <= 'Z') ? (c - 'A' + 1) : -1;
}

static int rangeStringToInt(const QString &string)
{
    int result = 0;
    const int size = string.size();
    for (int i = 0; i < size; i++) {
        //debugChart << "---" << float(rangeCharToInt(string[i].toLatin1()) * pow(10.0, (size - i - 1)));
        result += rangeCharToInt(string[i].toLatin1()) * pow(10.0, (size - i - 1));
    }
    //debugChart << "+++++ result=" << result;
    return result;
}

static QString rangeIntToString(int i)
{
    QString tmp = QString::number(i);
    for (int j = 0; j < tmp.size(); j++) {
        tmp[j] = 'A' + tmp[j].toLatin1() - '1';
    }

    //debugChart << "tmp=" << tmp;
    return tmp;
}
#endif

int CellRegion::rangeCharToInt(char c)
{
    return (c >= 'A' && c <= 'Z') ? (c - 'A' + 1) : -1;
}

int CellRegion::rangeStringToInt(const QString &string)
{
    int result = 0;
    const int size = string.size();
    for (int i = 0; i < size; i++) {
        result += rangeCharToInt(string[i].toLatin1()) * pow(10.0, (size - i - 1));
    }

    return result;
}

QString CellRegion::rangeIntToString(int i)
{
    QString tmp = QString::number(i);
    for (int j = 0; j < tmp.size(); j++) {
        tmp[j] = QChar('A' + tmp[j].toLatin1() - '1');
    }

    return tmp;
}

// Return the symbolic name of any column.
QString CellRegion::columnName(uint column)
{
    if (column < 1 || column > 32767)
        return QString("@@@");

    QString str;
    unsigned digits = 1;
    unsigned offset = 0;

    column--;

    for (unsigned limit = 26; column >= limit + offset; limit *= 26, ++digits)
        offset += limit;

    for (unsigned col = column - offset; digits; --digits, col /= 26)
        str.prepend(QChar('A' + (col % 26)));

    return str;
}

QDebug operator<<(QDebug dbg, const KoChart::CellRegion &r)
{
    dbg << "CellRegion[" << r.toString() << ']';
    return dbg;
}
