#include "QZXing.h"

#include <zxing/common/GlobalHistogramBinarizer.h>
#include <zxing/Binarizer.h>
#include <zxing/BinaryBitmap.h>
#include <zxing/MultiFormatReader.h>
#include <zxing/DecodeHints.h>
#include <zxing/ResultMetadata.h>
#include <zxing/common/detector/WhiteRectangleDetector.h>
#include <zxing/InvertedLuminanceSource.h>
#include "CameraImageWrapper.h"
#include "ImageHandler.h"
#include <QTime>
#include <QUrl>
#include <QFileInfo>
#include <QColor>
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
    #include <QtCore/QTextCodec>
#else
    #include <QStringDecoder>
#endif
#include <QDebug>

#ifdef ENABLE_ENCODER_QR_CODE
#include <zxing/qrcode/encoder/Encoder.h>
#include <zxing/qrcode/ErrorCorrectionLevel.h>
#endif // ENABLE_ENCODER_QR_CODE

#ifdef QZXING_MULTIMEDIA
#if QT_VERSION >= 0x060200
    #include "QZXingFilterVideoSink.h"
#else
    #include "QZXingFilter.h"
#endif //QT_VERSION
#endif //QZXING_MULTIMEDIA

#ifdef QZXING_QML
#if QT_VERSION >= 0x040700 && QT_VERSION < 0x050000
#include <QtDeclarative>
#elif QT_VERSION >= 0x050000
#include <QtQml/qqml.h>
#endif

#include <QQmlEngine>
#include <QQmlContext>
#include <QQuickImageProvider>
#include "QZXingImageProvider.h"
#endif //QZXING_QML


using namespace zxing;

QZXing::QZXing(QObject *parent) : QObject(parent), tryHarder_(false), lastDecodeOperationSucceded_(false)
{
    decoder = new MultiFormatReader();
    setDecoder(DecoderFormat_QR_CODE |
               DecoderFormat_DATA_MATRIX |
               DecoderFormat_UPC_E |
               DecoderFormat_UPC_A |
               DecoderFormat_UPC_EAN_EXTENSION |
               DecoderFormat_RSS_14 |
               DecoderFormat_RSS_EXPANDED |
               DecoderFormat_PDF_417 |
               DecoderFormat_MAXICODE |
               DecoderFormat_EAN_8 |
               DecoderFormat_EAN_13 |
               DecoderFormat_CODE_128 |
               DecoderFormat_CODE_93 |
               DecoderFormat_CODE_39 |
               DecoderFormat_CODABAR |
               DecoderFormat_ITF |
               DecoderFormat_Aztec);

    setTryHarderBehaviour(TryHarderBehaviour_Rotate |
                          TryHarderBehaviour_ThoroughScanning);

    setSourceFilterType(SourceFilter_ImageNormal);

    imageHandler = new ImageHandler();
}

QZXing::~QZXing()
{
    if (imageHandler)
        delete imageHandler;

    if (decoder)
        delete decoder;
}

QZXing::QZXing(QZXing::DecoderFormat decodeHints, QObject *parent) : QObject(parent), lastDecodeOperationSucceded_(false)
{
    decoder = new MultiFormatReader();
    imageHandler = new ImageHandler();

    setDecoder(decodeHints);
    setSourceFilterType(SourceFilter_ImageNormal);
}

#ifdef QZXING_QML

#if QT_VERSION >= 0x040700
void QZXing::registerQMLTypes()
{
    qmlRegisterType<QZXing>("QZXing", 3, 3, "QZXing");

#ifdef QZXING_MULTIMEDIA
    qmlRegisterType<QZXingFilter>("QZXing", 3, 3, "QZXingFilter");
#endif //QZXING_MULTIMEDIA

}
#endif //QT_VERSION >= Qt 4.7

#if  QT_VERSION >= 0x050000
void QZXing::registerQMLImageProvider(QQmlEngine& engine)
{
    engine.addImageProvider(QLatin1String("QZXing"), new QZXingImageProvider());
}
#endif //QT_VERSION >= Qt 5.0

#endif //QZXING_QML

void QZXing::setTryHarder(bool tryHarder)
{
    tryHarder_ = tryHarder;
}

bool QZXing::getTryHarder()
{
    return tryHarder_;
}

void QZXing::setTryHarderBehaviour(QZXing::TryHarderBehaviourType tryHarderBehaviour)
{
    tryHarderType = tryHarderBehaviour;
}

QZXing::TryHarderBehaviourType QZXing::getTryHarderBehaviour()
{
    return tryHarderType;
}

void QZXing::setSourceFilterType(QZXing::SourceFilterType sourceFilter)
{
    imageSourceFilter = sourceFilter;
}

QZXing::SourceFilterType QZXing::getSourceFilterType()
{
    return imageSourceFilter;
}
void QZXing::setAllowedExtensions(const QVariantList& extensions)
{
    std::set<int> allowedExtensions;
    for (const QVariant& extension: extensions) {
        allowedExtensions.insert(extension.toInt());
    }

    allowedExtensions_ = allowedExtensions;
}

QVariantList QZXing::getAllowedExtensions()
{
    QVariantList allowedExtensions;
    for (const int& extension: allowedExtensions_) {
        allowedExtensions << extension;
    }

    return allowedExtensions;
}

QString QZXing::decoderFormatToString(int fmt)
{
    switch (fmt) {
    case DecoderFormat_Aztec:
        return "AZTEC";

    case DecoderFormat_CODABAR:
        return "CODABAR";

    case DecoderFormat_CODE_39:
        return "CODE_39";

    case DecoderFormat_CODE_93:
        return "CODE_93";

    case DecoderFormat_CODE_128:
        return "CODE_128";

    case DecoderFormat_CODE_128_GS1:
        return "CODE_128_GS1";

    case DecoderFormat_DATA_MATRIX:
        return "DATA_MATRIX";

    case DecoderFormat_EAN_8:
        return "EAN_8";

    case DecoderFormat_EAN_13:
        return "EAN_13";

    case DecoderFormat_ITF:
        return "ITF";

    case DecoderFormat_MAXICODE:
        return "MAXICODE";

    case DecoderFormat_PDF_417:
        return "PDF_417";

    case DecoderFormat_QR_CODE:
        return "QR_CODE";

    case DecoderFormat_RSS_14:
        return "RSS_14";

    case DecoderFormat_RSS_EXPANDED:
        return "RSS_EXPANDED";

    case DecoderFormat_UPC_A:
        return "UPC_A";

    case DecoderFormat_UPC_E:
        return "UPC_E";

    case DecoderFormat_UPC_EAN_EXTENSION:
        return "UPC_EAN_EXTENSION";
    } // switch
    return QString();
}

QString QZXing::foundedFormat() const
{
    return decodedFormat;
}

QString QZXing::charSet() const
{
    return charSet_;
}

bool QZXing::getLastDecodeOperationSucceded()
{
    return lastDecodeOperationSucceded_;
}

QVariantMap QZXing::metadataToMap(const ResultMetadata &metadata)
{
    QVariantMap obj;
    for (const ResultMetadata::Key &key: metadata.keys()) {
        QString keyName = QString::fromStdString(metadata.keyToString(key));

        switch (key) {
        case ResultMetadata::ORIENTATION:
        case ResultMetadata::ISSUE_NUMBER:
        case ResultMetadata::STRUCTURED_APPEND_SEQUENCE:
        case ResultMetadata::STRUCTURED_APPEND_CODE_COUNT:
        case ResultMetadata::STRUCTURED_APPEND_PARITY:
            obj[keyName] = QVariant(metadata.getInt(key));
            break;
        case ResultMetadata::ERROR_CORRECTION_LEVEL:
        case ResultMetadata::SUGGESTED_PRICE:
        case ResultMetadata::POSSIBLE_COUNTRY:
        case ResultMetadata::UPC_EAN_EXTENSION:
            obj[keyName] = QVariant(metadata.getString(key).c_str());
            break;

        case ResultMetadata::OTHER:
        case ResultMetadata::PDF417_EXTRA_METADATA:
        case ResultMetadata::BYTE_SEGMENTS:
            break;
        }
    }

    return obj;
}

void QZXing::setDecoder(const uint &hint)
{
    unsigned int newHints = 0;

    if(hint & DecoderFormat_Aztec)
        newHints |= DecodeHints::AZTEC_HINT;

    if(hint & DecoderFormat_CODABAR)
        newHints |= DecodeHints::CODABAR_HINT;

    if(hint & DecoderFormat_CODE_39)
        newHints |= DecodeHints::CODE_39_HINT;

    if(hint & DecoderFormat_CODE_93)
        newHints |= DecodeHints::CODE_93_HINT;

    if(hint & DecoderFormat_CODE_128)
        newHints |= DecodeHints::CODE_128_HINT;

    if(hint & DecoderFormat_DATA_MATRIX)
        newHints |= DecodeHints::DATA_MATRIX_HINT;

    if(hint & DecoderFormat_EAN_8)
        newHints |= DecodeHints::EAN_8_HINT;

    if(hint & DecoderFormat_EAN_13)
        newHints |= DecodeHints::EAN_13_HINT;

    if(hint & DecoderFormat_ITF)
        newHints |= DecodeHints::ITF_HINT;

    if(hint & DecoderFormat_MAXICODE)
        newHints |= DecodeHints::MAXICODE_HINT;

    if(hint & DecoderFormat_PDF_417)
        newHints |= DecodeHints::PDF_417_HINT;

    if(hint & DecoderFormat_QR_CODE)
        newHints |= DecodeHints::QR_CODE_HINT;

    if(hint & DecoderFormat_RSS_14)
        newHints |= DecodeHints::RSS_14_HINT;

    if(hint & DecoderFormat_RSS_EXPANDED)
        newHints |= DecodeHints::RSS_EXPANDED_HINT;

    if(hint & DecoderFormat_UPC_A)
        newHints |= DecodeHints::UPC_A_HINT;

    if(hint & DecoderFormat_UPC_E)
        newHints |= DecodeHints::UPC_E_HINT;

    if(hint & DecoderFormat_UPC_EAN_EXTENSION)
        newHints |= DecodeHints::UPC_EAN_EXTENSION_HINT;

    if(hint & DecoderFormat_CODE_128_GS1)
    {
        newHints |= DecodeHints::CODE_128_HINT;
        newHints |= DecodeHints::ASSUME_GS1;
    }

    enabledDecoders = newHints;

    emit enabledFormatsChanged();
}

/*!
 * \brief getTagRec - returns rectangle containing the tag.
 *
 * To be able display tag rectangle regardless of the size of the bit matrix rect is in related coordinates [0; 1].
 * \param resultPoints
 * \param bitMatrix
 * \return
 */
QRectF getTagRect(const QSharedPointer<std::vector<QSharedPointer<ResultPoint>> > &resultPoints, const QSharedPointer<BitMatrix> &bitMatrix)
{
    if (resultPoints->size() < 2)
        return QRectF();

    int matrixWidth = bitMatrix->getWidth();
    int matrixHeight = bitMatrix->getHeight();
    // 1D barcode
    if (resultPoints->size() == 2) {
        WhiteRectangleDetector detector(bitMatrix);
        std::vector<QSharedPointer<ResultPoint> > resultRectPoints = detector.detect();

        if (resultRectPoints.size() != 4)
            return QRectF();

        qreal xMin = qreal((*resultPoints)[0]->getX());
        qreal xMax = xMin;
        for (int i = 1; i < resultPoints->size(); ++i) {
            qreal x = qreal((*resultPoints)[i]->getX());
            if (x < xMin)
                xMin = x;
            if (x > xMax)
                xMax = x;
        }

        qreal yMin = qreal(resultRectPoints[0]->getY());
        qreal yMax = yMin;
        for (size_t i = 1; i < resultRectPoints.size(); ++i) {
            qreal y = qreal(resultRectPoints[i]->getY());
            if (y < yMin)
                yMin = y;
            if (y > yMax)
                yMax = y;
        }

        return QRectF(QPointF(xMin / matrixWidth, yMax / matrixHeight), QPointF(xMax / matrixWidth, yMin / matrixHeight));
    }

    // 2D QR code
    if (resultPoints->size() == 4) {
        qreal xMin = qreal((*resultPoints)[0]->getX());
        qreal xMax = xMin;
        qreal yMin = qreal((*resultPoints)[0]->getY());
        qreal yMax = yMin;
        for (int i = 1; i < resultPoints->size(); ++i) {
            qreal x = qreal((*resultPoints)[i]->getX());
            qreal y = qreal((*resultPoints)[i]->getY());
            if (x < xMin)
                xMin = x;
            if (x > xMax)
                xMax = x;
            if (y < yMin)
                yMin = y;
            if (y > yMax)
                yMax = y;
        }

        return QRectF(QPointF(xMin / matrixWidth, yMax / matrixHeight), QPointF(xMax / matrixWidth, yMin / matrixHeight));
    }

    return QRectF();
}

QString QZXing::decodeImage(const QImage &image, int maxWidth, int maxHeight, bool smoothTransformation)
{
    //qDebug() << "Start decoding";
    QElapsedTimer t;
    t.start();
    processingTime = -1;
    QSharedPointer<Result> res;
    emit decodingStarted();

    if(image.isNull())
    {
        processingTime = t.elapsed();
        emit decodingFinished(false);
        //qDebug() << "End decoding 1";
        return "";
    }

    QSharedPointer<CameraImageWrapper> ciw;

    if ((maxWidth > 0) || (maxHeight > 0))
        ciw = CameraImageWrapper::Factory(image, maxWidth, maxHeight, smoothTransformation);
    else
        ciw = CameraImageWrapper::Factory(image, 999, 999, true);

    QString errorMessage = "Unknown";

    QSharedPointer<LuminanceSource> imageRefOriginal = ciw;
    QSharedPointer<LuminanceSource> imageRef = imageRefOriginal;
    QSharedPointer<GlobalHistogramBinarizer> binz;
    QSharedPointer<BinaryBitmap> bb;

    size_t numberOfIterations = 0;
    if (imageSourceFilter & SourceFilter_ImageNormal)
        numberOfIterations++;
    if (imageSourceFilter & SourceFilter_ImageInverted)
        numberOfIterations++;

    //qDebug() << "Iterations: "<< numberOfIterations << ", sourceFilter: " << imageSourceFilter;

    for(size_t i=0; i<numberOfIterations; ++i){
        try {
            if((numberOfIterations == 1 && (imageSourceFilter & SourceFilter_ImageInverted)) || i == 1) {
                //qDebug() << "Selecting Inverted Luminance source";
                imageRef = QSharedPointer<LuminanceSource>((LuminanceSource*)(new InvertedLuminanceSource(imageRefOriginal)));
            }
            binz = QSharedPointer<GlobalHistogramBinarizer>( new GlobalHistogramBinarizer(imageRef) );
            bb = QSharedPointer<BinaryBitmap>( new BinaryBitmap(binz) );

            DecodeHints hints(static_cast<DecodeHintType>(enabledDecoders));

            if (hints.containsFormat(BarcodeFormat::UPC_EAN_EXTENSION)) {
                hints.setAllowedEanExtensions(allowedExtensions_);
            }

            lastDecodeOperationSucceded_ = false;
            try {
                //qDebug() << "Decoding phase 1: started";
                res = decoder->decode(bb, hints);
                processingTime = t.elapsed();
                lastDecodeOperationSucceded_ = true;
                break;
            } catch(zxing::Exception &/*e*/){
                //qDebug() << "Decoding phase 1: failed";
            }

            if(!lastDecodeOperationSucceded_ && tryHarder_ && (tryHarderType & TryHarderBehaviour_ThoroughScanning))
            {
                //qDebug() << "Decoding phase 2, thorought scan: starting";
                hints.setTryHarder(true);
                if(hints.containsFormat(BarcodeFormat::UPC_EAN_EXTENSION) &&
                        !allowedExtensions_.empty() &&
                        !(hints & DecodeHints::PRODUCT_HINT).isEmpty() )
                    hints.setAllowedEanExtensions(std::set<int>());

                try {
                    res = decoder->decode(bb, hints);
                    processingTime = t.elapsed();
                    lastDecodeOperationSucceded_ = true;
                    break;
                } catch(zxing::Exception &/*e*/) {
                    //qDebug() << "Decoding phase 2, thorought scan: failed";
                }
            }

            if (!lastDecodeOperationSucceded_&& tryHarder_ && (tryHarderType & TryHarderBehaviour_Rotate) && bb->isRotateSupported()) {
                QSharedPointer<BinaryBitmap> bbTmp = bb;

                //qDebug() << "Decoding phase 2, rotate: starting";

                hints.setTryHarder(true);
                for (int i=0; (i<3 && !lastDecodeOperationSucceded_); i++) {
                    QSharedPointer<BinaryBitmap> rotatedImage(bbTmp->rotateCounterClockwise());
                    bbTmp = rotatedImage;

                    try {
                        res = decoder->decode(rotatedImage, hints);
                        processingTime = t.elapsed();
                        lastDecodeOperationSucceded_ = true;
                        break;
                    } catch(zxing::Exception &/*e*/) {
                        //qDebug() << "Decoding phase 2, rotate: failed";
                    }
                }
            }
        }
        catch(zxing::Exception &e)
        {
            errorMessage = QString(e.what());
            //qDebug() << "Decoding failed: " << errorMessage;
        }
    }

    if (lastDecodeOperationSucceded_) {
        //qDebug() << "Decoding succeeded.";
        QString string = QString(res->getText()->getText().c_str());
        if (!string.isEmpty() && (string.length() > 0)) {
            int fmt = res->getBarcodeFormat().value;
            decodedFormat = decoderFormatToString(1<<fmt);
            charSet_ = QString::fromStdString(res->getCharSet());
            if (!charSet_.isEmpty()) {
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
                    QTextCodec *codec = QTextCodec::codecForName(res->getCharSet().c_str());
                    if (codec)
                        string = codec->toUnicode(res->getText()->getText().c_str());
#else
                    QStringDecoder decoder(res->getCharSet().c_str());
                    if(decoder.isValid()) {
                        string = decoder.decode(QByteArray(res->getText()->getText().c_str()));
                    }
#endif
            }

            emit tagFound(string);
            emit tagFoundAdvanced(string, decodedFormat, charSet_);

            QVariantMap metadataMap = metadataToMap(res->getMetadata());
            emit tagFoundAdvanced(string, decodedFormat, charSet_, metadataMap);

            try {
                const QRectF rect = getTagRect(res->getResultPoints(), binz->getBlackMatrix());
                emit tagFoundAdvanced(string, decodedFormat, charSet_, rect);
            }catch(zxing::Exception &/*e*/){}
        }
        processingTime = t.elapsed();
        emit decodingFinished(true);
        return string;
    }

    processingTime = t.elapsed();
    emit error(errorMessage);
    emit decodingFinished(false);
    return "";
}

QString QZXing::decodeImageFromFile(const QString& imageFilePath, int maxWidth, int maxHeight, bool smoothTransformation)
{
    // used to have a check if this image exists
    // but was removed because if the image file path doesn't point to a valid image
    // then the QImage::isNull will return true and the decoding will fail eitherway.
    const QString header = "file://";

    QString filePath = imageFilePath;
    if(imageFilePath.startsWith(header))
        filePath = imageFilePath.right(imageFilePath.size() - header.size());

    QUrl imageUrl = QUrl::fromLocalFile(filePath);
    QImage tmpImage = QImage(imageUrl.toLocalFile());
    return decodeImage(tmpImage, maxWidth, maxHeight, smoothTransformation);
}

QString QZXing::decodeImageQML(QObject *item)
{
    return decodeSubImageQML(item);
}

QString QZXing::decodeSubImageQML(QObject *item,
                                  const int offsetX, const int offsetY,
                                  const int width, const int height)
{
    if(item  == ZXING_NULLPTR)
    {
        processingTime = 0;
        emit decodingFinished(false);
        return "";
    }

    QImage img = imageHandler->extractQImage(item, offsetX, offsetY, width, height);

    return decodeImage(img);
}

QString QZXing::decodeImageQML(const QUrl &imageUrl)
{
    return decodeSubImageQML(imageUrl);
}

QString QZXing::decodeSubImageQML(const QUrl &imageUrl,
                                  const int offsetX, const int offsetY,
                                  const int width, const int height)
{
#ifdef QZXING_QML

    QString imagePath = imageUrl.path();
    imagePath = imagePath.trimmed();
    QImage img;
    if (imageUrl.scheme() == "image") {
        if (imagePath.startsWith("/"))
            imagePath = imagePath.right(imagePath.length() - 1);
        QQmlEngine *engine = QQmlEngine::contextForObject(this)->engine();
        QQuickImageProvider *imageProvider = dynamic_cast<QQuickImageProvider *>(engine->imageProvider(imageUrl.host()));
        QSize imgSize;
        img = imageProvider->requestImage(imagePath, &imgSize, QSize());
    } else {
        QFileInfo fileInfo(imagePath);
        if (!fileInfo.exists()) {
            qDebug() << "[decodeSubImageQML()] The file" << imagePath << "does not exist.";
            emit decodingFinished(false);
            return "";
        }
        img = QImage(imagePath);
    }

    if (offsetX || offsetY || width || height)
        img = img.copy(offsetX, offsetY, width, height);
    return decodeImage(img);
#else
    Q_UNUSED(imageUrl);
    Q_UNUSED(offsetX);
    Q_UNUSED(offsetY);
    Q_UNUSED(width);
    Q_UNUSED(height);
    return decodeImage(QImage());
#endif //QZXING_QML
}

#ifdef ENABLE_ENCODER_GENERIC
QImage QZXing::encodeData(const QString& data,
                          const EncoderFormat encoderFormat,
                          const QSize encoderImageSize,
                          const EncodeErrorCorrectionLevel errorCorrectionLevel,
                          const bool border,
                          const bool transparent)
{
    return encodeData(data,
                      QZXingEncoderConfig(encoderFormat,
                                          encoderImageSize,
                                          errorCorrectionLevel,
                                          border,
                                          transparent));
}

QImage QZXing::encodeData(const QString &data, const QZXingEncoderConfig &encoderConfig)
{
    QImage image;

    try {
        switch (encoderConfig.format) {
#ifdef ENABLE_ENCODER_QR_CODE
        case EncoderFormat_QR_CODE:
        {
            QSharedPointer<qrcode::QRCode> barcode = qrcode::Encoder::encode(
                        data.toStdWString(),
                        encoderConfig.errorCorrectionLevel == EncodeErrorCorrectionLevel_H ?
                            qrcode::ErrorCorrectionLevel::H :
                            (encoderConfig.errorCorrectionLevel == EncodeErrorCorrectionLevel_Q ?
                                 qrcode::ErrorCorrectionLevel::Q :
                                 (encoderConfig.errorCorrectionLevel == EncodeErrorCorrectionLevel_M ?
                                      qrcode::ErrorCorrectionLevel::M :
                                      qrcode::ErrorCorrectionLevel::L)));

            QSharedPointer<qrcode::ByteMatrix> bytesRef = barcode->getMatrix();
            const std::vector< std::vector <zxing::byte> >& bytes = bytesRef->getArray();
            const int width = int(bytesRef->getWidth()) + (encoderConfig.border ? 2 : 0);
            const int height = int(bytesRef->getHeight()) + (encoderConfig.border ? 2 : 0);
            const QRgb black = qRgba(0, 0, 0, encoderConfig.transparent ? 0 : 255);
            const QRgb white = qRgba(255, 255, 255, 255);

            image = QImage(width, height, QImage::Format_ARGB32);
            image.fill(white);

            int offset = encoderConfig.border ? 1 : 0;

            for (size_t i=0; i<bytesRef->getWidth(); ++i) {
                for (size_t j=0; j<bytesRef->getHeight(); ++j) {
                    if (bytes[j][i]) {
                        image.setPixel(offset+int(i), offset+int(j), black);
                    }
                }
            }

            image = image.scaled(encoderConfig.imageSize);
            break;
        }
#endif // ENABLE_ENCODER_QR_CODE
        case EncoderFormat_INVALID:
            break;
        }
    } catch (std::exception& e) {
        std::cout << "Error: " << e.what() << std::endl;
    }

    return image;
}
#endif // ENABLE_ENCODER_GENERIC

int QZXing::getProcessTimeOfLastDecoding()
{
    return processingTime;
}

uint QZXing::getEnabledFormats() const
{
    return enabledDecoders;
}