/****************************************************************************
**
** Copyright (C) 2015 The Qt Company Ltd.
** Contact: http://www.qt.io/licensing/
**
** This file is part of the Qt Messaging Framework.
**
** $QT_BEGIN_LICENSE:LGPL21$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see http://www.qt.io/terms-conditions. For further
** information use the contact form at http://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 2.1 or version 3 as published by the Free
** Software Foundation and appearing in the file LICENSE.LGPLv21 and
** LICENSE.LGPLv3 included in the packaging of this file. Please review the
** following information to ensure the GNU Lesser General Public License
** requirements will be met: https://www.gnu.org/licenses/lgpl.html and
** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
**
** As a special exception, The Qt Company gives you certain additional
** rights. These rights are described in The Qt Company LGPL Exception
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
**
** $QT_END_LICENSE$
**
****************************************************************************/

#include "integerregion.h"
#include <qmaillog.h>
#include <QRegularExpression>

/*
  \class IntegerRegion
  \brief The IntegerRegion class provides a one dimensional integer region 
  i.e. a sequence of integer ranges.

  Instantiations can be used to store IMAP sequences sets as described
  in RFC3501.

  Currently the internal representation consists of a list of inclusive 
  ranges stored in ascending order. A range is stored as a qpair of ints, 
  the first int in the pair being the beginning of the range (low value) 
  and the second being the end of the range (high value). A single integer 
  is stored as a range with the start and end being equal. Ranges are 
  non-overlapping, and non-adjacent i.e. there is a gap of at least 
  length one between ranges.
  
  If useful this class could be extended to contain new methods such as
  left (first n integers contained), mid, right, union, intersection, 
  at (nth integer contained). cardinality could be optimized.
*/

/*!
    Constructs an empty IntegerRegion object.
*/
IntegerRegion::IntegerRegion()
{
}

/*
  Constructs an IntegerRegion object, from \a uids, which may be unordered.
*/
IntegerRegion::IntegerRegion(const QStringList &uids)
{
    // Performance note currently O(n^2), n = uids.count()
    // TODO: sort uids if they are not already sorted
    foreach(const QString &uid, uids) {
        bool ok(false);
        uint number = uid.toUInt(&ok);
        if (ok)
            add(number);
    }
}

/*
  Constructs an IntegerRegion object, from \a uidString an IMAP sequence-set
  string as described in RFC3501. \a uidString may contain an unordered 
  list of ids, wildcards are not currently supported.
*/
IntegerRegion::IntegerRegion(const QString &uidString)
{
    // Performance note currently O(n^2), n = uids.count()
    // TODO: sort uids in uidString if they are not already sorted
    QStringList rangeList = uidString.split(",", Qt::SkipEmptyParts);
    foreach(const QString &s, rangeList) {
        bool ok = false;
        int index = s.indexOf(":");
        if (index == -1) {
            int a = s.toInt(&ok);
            if (!ok)
                continue;
            add(a);
        } else if (index > 0) {
            int a = s.left(index).toInt(&ok);
            if (!ok)
                continue;
            int b = s.mid(index+1).toInt(&ok);
            if (!ok)
                continue;
            for(int i = a; i <= b; ++i) {
                // could be optimized if union is implemented
                add(i);
            }
        }
    }
}

IntegerRegion::IntegerRegion(int begin, int end)
{
    clear();
    if (begin > end)
        return;
    mRangeList.append(IntegerRange(begin, end));
}

/*
  Removes all ranges from the region.
*/
void IntegerRegion::clear()
{
    return mRangeList.clear();
}

/*
  Returns true if the region contains no integers; otherwise returns false.
*/
bool IntegerRegion::isEmpty() const
{
    return mRangeList.isEmpty();
}

/*
  Number of integers contained in the region. Could be optimized by keeping 
  a running count in a member variable.
*/
//  Maybe count would be a better name.
uint IntegerRegion::cardinality() const
{
    uint result(0);

    foreach( const IntegerRange &range, mRangeList)
        result += range.second - range.first + 1;

    return result;
}

/*
  The maximum integer contained in the region. The region must be not
  be empty. If the region can be empty call isEmpty() before calling this function.
*/
int IntegerRegion::maximum() const
{
    return mRangeList.last().second;
}

/*
  The minimum integer contained in the region. The region must be not
  be empty. If the region can be empty call isEmpty() before calling this function.
*/
int IntegerRegion::minimum() const
{
    return mRangeList.first().first;
}

/*
  Returns a string list of integers contained in the region.
*/
QStringList IntegerRegion::toStringList() const
{
    QStringList result;
    foreach(const IntegerRange &range, mRangeList) {
        result += QString::number(range.first);
        for (int i = range.first + 1; i <= range.second; ++i)
            result += QString::number(i);
    }
    return result;
}

/*
  Returns a sequence-set string as described in RFC3501.
*/
QString IntegerRegion::toString() const
{
    QString result;
    bool first(true);
    foreach(const IntegerRange &range, mRangeList) {
        if (!first)
            result += ",";
        result += QString::number(range.first);
        if (range.second > range.first)
            result += QString(":%1").arg(range.second);
        first = false;
    }
    return result;
}

/*
  Inserts \a number into the integer region if it is not already contained; 
  otherwise does nothing.
  
  Currently optimized for appending integers greater than any contained by 
  the region.
*/
void IntegerRegion::add(int number)
{
    // Start from the end of the list of ranges since it's expected that 
    // normally numbers above the current range contained will be added.
    QList< IntegerRange >::iterator previous = mRangeList.end();
    QList< IntegerRange >::iterator next;

    while (previous != mRangeList.begin()) {
        next = previous;
        --previous;
        const int first = (*previous).first;
        const int second = (*previous).second;
        if (number < first - 1) {
            continue;
        } else if (number > second + 1) {
            // insert new range between previous and next item
            mRangeList.insert(next, IntegerRange(number, number));
            return;
        } else if (number == second + 1) {
            // increment upper bound
            (*previous).second = number;
            return;
        } else if ((number >= first) && (number <= second)) {
            // already contained nothing todo
            return;
        } else if (number == first - 1) {
            if (previous == mRangeList.begin()) {
                // decrement lower bound first item
                (*previous).first = number;
                return;
            }

            next = previous;
            --previous;
            if ((*previous).second == first - 2) {
                // coalesce current and previous item
                (*previous).second = (*next).second;
                mRangeList.erase(next);
            } else {
                // decrement lower bound
                (*next).first = number;
            }
            return;
        }
    }
    // insert new item at start of list
    mRangeList.insert(previous, IntegerRange(number, number));
}

/*
  Returns a region containing all integers in this region that are not also 
  in the \a other region.
*/
IntegerRegion IntegerRegion::subtract(IntegerRegion other) const
{
    // Performance note O(n), n = max(a.cardinality(), b.cardinality())
    IntegerRegion result(*this);
    QList< IntegerRange >::iterator a = result.mRangeList.begin();
    QList< IntegerRange >::iterator b = other.mRangeList.begin();
    
    while (a != result.mRangeList.end()
           && b != other.mRangeList.end()) {
        if ((*b).second < (*a).first) {
            // b < a, strictly
            ++b;
            continue;
        } else if ((*b).first > (*a).second) {
            // b > a, strictly
            ++a;
            continue;
        } else if (((*b).first <= (*a).first) && ((*b).second >= (*a).second)) {
            // b contains a
            a = result.mRangeList.erase(a);
            continue;
        } else if (((*b).first > (*a).first) && ((*b).second < (*a).second)) {
            // a strictly contains b
            IntegerRange lowerSlice((*a).first, (*b).first - 1);
            a = result.mRangeList.insert(a, lowerSlice);
            ++a;
            (*a).first = (*b).second + 1;
            ++b;
            continue;
        } else if (((*b).first <= (*a).first) && ((*b).second < (*a).second)) {
            // b < a, but overlap
            (*a).first = (*b).second + 1;
            ++b;
        } else if (((*b).first <= (*a).second) && ((*b).second >= (*a).second)) {
            // b > a, but overlap
            (*a).second = (*b).first - 1;
            ++a;
        } else {
            qWarning() << "Unhandled IntegerRegion case a " << *a << " b " << *b;
            return result;
        }
    }
    return result;
}

/*
  Returns the union of this region and the \a other region.
*/
IntegerRegion IntegerRegion::add(IntegerRegion other) const
{
    if (!cardinality())
        return other;
    
    if (!other.cardinality())
        return *this;
    
    int min = qMin(minimum(), other.minimum());
    int max = qMax(maximum(), other.maximum());
    IntegerRegion c(min, max);
    // a + b = c - (c - a - b)
    return c.subtract(c.subtract(*this).subtract(other));
}

/*
  Returns the intersection of this region and the \a other region.
*/
IntegerRegion IntegerRegion::intersect(IntegerRegion other) const
{
    IntegerRegion A(*this);
    IntegerRegion B(other);
    // A n B = (A U B) - ((A - B) U (B - A)) 
    IntegerRegion result(A.add(B).subtract(A.subtract(B).add(B.subtract(A))));
    return result;
}

/*
  Returns true if \a uids contains a list of integers; otherwise returns false.
*/
bool IntegerRegion::isIntegerRegion(QStringList uids)
{
    foreach(const QString &uid, uids) {
        bool ok(false);
        uid.toUInt(&ok);
        if (!ok)
            return false;
    }
    return true;
}

/* 
    Returns the list of integers contained by the description \a region.
*/
QList<int> IntegerRegion::toList(const QString &region)
{
    QList<int> result;

    int index = 0;
    QRegularExpression range("(\\d+)(?::(\\d+))?(?:,)?");
    QRegularExpressionMatch match = range.match(region, index);

    while (match.hasMatch()) {
        index += match.captured(0).length();
        int first = match.captured(1).toInt();
        int second = first;
        if (!match.captured(2).isEmpty()) {
            second = match.captured(2).toInt();
            if (second < first) {
                second = first;
            }
        }

        for ( ; first <= second; ++first) {
            result.append(first);
        }

        match = range.match(region, index);
    }

    return result;
}


#if 0
//TODO Convert these tests to standard qunit style

/*
  Test function.
  
  Returns an integer region containing all integers marked in the binary 
  string \s, counting from zero.
*/
IntegerRegion IntegerRegion::fromBinaryString(const QString &s)
{
    IntegerRegion result;
    for (int i = 0; i < s.length(); ++i) {
        if (!s[i].isSpace())
            result.add(i);
    }
    return result;
}

/*
  Test function.
  
  Returns a binary string of all integers contained by \ir, counting from zero.
*/
QString IntegerRegion::toBinaryString(const IntegerRegion &ir)
{
    QString result;
    int last(0);
    foreach(const QString &s, ir.toStringList()) {
        bool ok;
        int value = s.toInt(&ok);
        for (int i = last; i < value; ++i)
            result += ' ';
        result += '-';
        last = value + 1;
    }
    return result;
}

/*
  Test function.
  
  Run through some tests.
*/
int IntegerRegion::tests()
{
    QList<int> values;
    values << 12 << 13 << 16 << 20 << 22 << 23 << 24 << 27 << 28 << 34;

    QStringList list;
    foreach (const int &v, values) {
        list << QString::number(v);
    }

    qMailLog(Messaging) << "Constructing from QStringList " << list;
    
    IntegerRegion ir(list);
    qMailLog(Messaging) << "IntegerRegion = " << ir.toString();
    qMailLog(Messaging) << "IntegerRegion::IntegerRegion(QStringList) test1"
                        << ((ir.toString() == "12:13,16,20,22:24,27:28,34") ? "passed" : "failed");
    qMailLog(Messaging) << "IntegerRegion::toList(QString) test1"
                        << ((IntegerRegion::toList(ir.toString()) == values) ? "passed" : "failed");
    qMailLog(Messaging) << "Adding 31. Insert new region between existing regions.";
    ir.add(31);
    qMailLog(Messaging) << "IntegerRegion = " << ir.toString();
    qMailLog(Messaging) << "Adding 14. Increment first region.";
    ir.add(14);
    qMailLog(Messaging) << "IntegerRegion = " << ir.toString();
    qMailLog(Messaging) << "Adding 13. Ignore.";
    ir.add(13);
    qMailLog(Messaging) << "IntegerRegion = " << ir.toString();
    qMailLog(Messaging) << "Adding 11. Decrement first region.";
    ir.add(11);
    qMailLog(Messaging) << "IntegerRegion = " << ir.toString();
    qMailLog(Messaging) << "Adding 15. Decrement second region and coalesce.";
    ir.add(15);
    qMailLog(Messaging) << "IntegerRegion = " << ir.toString();
    qMailLog(Messaging) << "Adding 26. Decrement lower bound.";
    ir.add(26);
    qMailLog(Messaging) << "IntegerRegion = " << ir.toString();
    qMailLog(Messaging) << "Adding 7. Insert new region at start of list";
    ir.add(7);
    qMailLog(Messaging) << "IntegerRegion = " << ir.toString();
    qMailLog(Messaging) << "Adding 42. Insert new region at end";
    ir.add(42);
    qMailLog(Messaging) << "IntegerRegion " << ir.toString();
    qMailLog(Messaging) << "Adding 43. Increment last region.";
    ir.add(43);
    qMailLog(Messaging) << "IntegerRegion = " << ir.toString();
    qMailLog(Messaging) << "Adding 25. Another coalesce.";
    ir.add(25);
    qMailLog(Messaging) << "IntegerRegion = " << ir.toString();
    qMailLog(Messaging) << "IntegerRegion::toString test2"
                        << ((ir.toString() == "7,11:16,20,22:28,31,34,42:43") ? "passed" : "failed");
    qMailLog(Messaging) << "IntegerRegion::cardinality test3"
                        << ((ir.cardinality() == 19) ? "passed" : "failed");

    qMailLog(Messaging) << "IntegerRegion cardinality() = " << ir.cardinality();
    qMailLog(Messaging) << "IntegerRegion toStringList() = " << ir.toStringList();
    qMailLog(Messaging) << "IntegerRegion::isIntegerRegion test4: " 
                    << (IntegerRegion::isIntegerRegion(ir.toStringList()) ? "passed" : "failed");
    QStringList hippo;
    hippo << "1" << "3" << "hippo" << "7";
    qMailLog(Messaging) << "IntegerRegion::isIntegerRegion test5: " 
                    << (IntegerRegion::isIntegerRegion(hippo) ? "failed" : "passed");

    QList<int> newValues;
    foreach (const QString &number, ir.toStringList()) {
        newValues << number.toInt();
    }
    qMailLog(Messaging) << "IntegerRegion::toList(QString) test2"
                        << ((IntegerRegion::toList(ir.toString()) == newValues) ? "passed" : "failed");

    QString a("    --  -  --  --- -- -- -   --   -- -  -");
    QString b("  - -- -- ----  -   - -   -   -- --  -   ");
    QString c("               - - -   - -   -     -    -");
    IntegerRegion ar = IntegerRegion::fromBinaryString(a);
    IntegerRegion br = IntegerRegion::fromBinaryString(b);
    IntegerRegion cr = ar.subtract(br);
    qMailLog(Messaging) << "a " << ar.toString();
    qMailLog(Messaging) << "b " << br.toString();
    qMailLog(Messaging) << "a   " << IntegerRegion::toBinaryString(ar) << " - ";
    qMailLog(Messaging) << "b   " << IntegerRegion::toBinaryString(br) << " = ";
    qMailLog(Messaging) << "a-b " << IntegerRegion::toBinaryString(cr);
    qMailLog(Messaging) << "c   " << c;
    qMailLog(Messaging) << "IntegerRegion::subtract test6: " 
                        << ((IntegerRegion::toBinaryString(cr) == c) ? "passed" : "failed");
    QString a2("     ---  --  ----  ----- ---- ---- ---     ---     --- --  -");
    QString b2("  -  --- --- ------  ---    -- --      ---    --- ---   --   ");
    QString c2("                    -   - --     -- ---     --       --     -");
    ar = IntegerRegion::fromBinaryString(a2);
    br = IntegerRegion::fromBinaryString(b2);
    cr = ar.subtract(br);
    qMailLog(Messaging) << "a2 " << ar.toString();
    qMailLog(Messaging) << "b2 " << br.toString();
    qMailLog(Messaging) << "a   " << IntegerRegion::toBinaryString(ar) << " - ";
    qMailLog(Messaging) << "b   " << IntegerRegion::toBinaryString(br) << " = ";
    qMailLog(Messaging) << "a-b " << IntegerRegion::toBinaryString(cr);
    qMailLog(Messaging) << "c   " << c2;
    qMailLog(Messaging) << "IntegerRegion::subtract test7: " 
                        << ((IntegerRegion::toBinaryString(cr) == c2) ? "passed" : "failed");

    QString a3("1:7,9:15");
    QString b3("2:5,10:13,7,1,6,9,12:15");
    ar = IntegerRegion(b3);
    qMailLog(Messaging) << "b3 " << b3;
    qMailLog(Messaging) << "c3 " << ar.toString();
    qMailLog(Messaging) << "IntegerRegion::fromString test8: " 
                        << ((ar.toString() == a3) ? "passed" : "failed");


    values.clear();
    list.clear();
    values << 15555 << 15556 << 15557 << 15558 << 15559 << 15561 << 15562 << 15563 << 15565 
           << 15566 << 15567 << 15569 << 15573 << 15578 << 15579 << 15580 << 15581 << 15582 
           << 15584 << 15586 << 15587 << 15590 << 15593 << 15595 << 15596 << 15599 << 15600 
           << 15602 << 15605 << 15606 << 15607 << 15609;

    foreach (const int &v, values) {
        list << QString::number(v);
    }
    
    ir = IntegerRegion(list);
    IntegerRegion jr(ir.minimum(), ir.maximum());
    ar = jr.subtract(ir);
    QString a4("15560,15564,15568,15570:15572,15574:15577,15583,15585,15588:15589,15591:15592,15594,15597:15598,15601,15603:15604,15608");
    
    qMailLog(Messaging) << "IntegerRegion::subtractTest test9: " 
                        << ((ar.toString() == a4) ? "passed" : "failed");
    
    ir = IntegerRegion("1:20");
    jr = IntegerRegion("10:30");
    ar = jr.intersect(ir);
    QString a5("10:20");
    qMailLog(Messaging) << "IntegerRegion::intersectionTest test10: " 
                        << ((ar.toString() == a5) ? "passed" : "failed");

    ar = ir.intersect(jr);
    qMailLog(Messaging) << "IntegerRegion::intersectionTest test11: " 
                        << ((ar.toString() == a5) ? "passed" : "failed");
    
    ir = IntegerRegion("1:10");
    jr = IntegerRegion("20:30");
    ar = jr.intersect(ir);
    QString a6("");
    qMailLog(Messaging) << "IntegerRegion::intersectionTest test12: " 
                        << ((ar.toString() == a6) ? "passed" : "failed");

    ir = IntegerRegion("1:10");
    jr = IntegerRegion("20:30");
    ar = ir.intersect(jr);
    QString a7("");
    qMailLog(Messaging) << "IntegerRegion::intersectionTest test13: " 
                        << ((ar.toString() == a7) ? "passed" : "failed");

    ar = ir.intersect(ir);
    QString a8("1:10");
    qMailLog(Messaging) << "IntegerRegion::intersectionTest test14: " 
                        << ((ar.toString() == a8) ? "passed" : "failed");
    
    ir = IntegerRegion("1:4,6:8,9,10,30,1000,20000,30000");
    jr = IntegerRegion("1:30000");
    ar = ir.intersect(jr);
    qMailLog(Messaging) << "IntegerRegion::intersectionTest test15: " 
                        << ((ar.toString() == ir.toString()) ? "passed" : "failed");
    ar = jr.intersect(ir);
    qMailLog(Messaging) << "IntegerRegion::intersectionTest test16: " 
                        << ((ar.toString() == ir.toString()) ? "passed" : "failed");
    
    jr = IntegerRegion("2,5,7,100:10000,25000,26000,27000,28000");
    ar = jr.intersect(ir);
    QString a9("2,7,1000");
    qMailLog(Messaging) << "IntegerRegion::intersectionTest test17: " 
                        << ((ar.toString() == a9) ? "passed" : "failed");
    
    ar = ir.intersect(jr);
    qMailLog(Messaging) << "IntegerRegion::intersectionTest test18: " 
                        << ((ar.toString() == a9) ? "passed" : "failed");

    return 1;
}

static const int testsRun = IntegerRegion::tests();
#endif
