#include "imagecompare.h"
#include "border.h"
#include "uimanager.h"
#include "fileop.h"
#include "imageutils.h"
#include "ifapp.h"
#include "imageheaders.h"
#include <qdir.h>
#include <qfileinfo.h>
#include <qimage.h>
#include <qlayout.h>
#include <qpushbutton.h>
#include <qlabel.h>
#include <qfontmetrics.h>
#include <qpainter.h>
#include <qclipboard.h>
#include <kmessagebox.h>
#include <klocale.h>
#include <kmimetype.h>
#include <kprogress.h>
#include <kstatusbar.h>
#include <kdirwatch.h>
#include <kpopupmenu.h>
#include <kiconloader.h>
#include <kimageeffect.h>

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <string.h>

KIFCompare::KIFCompare(const QString &path, int iconSize, UIManager *manager,
                       QWidget *parent, const char *name)
    : QSemiModal(parent, name, true, WDestructiveClose)
{
    setCaption(i18n("Finding Similiar Images"));
    testTime.start();

    stopProcessing = false;
    dirPath = path;
    iSize = iconSize;
    mgr = manager;
    fileList.resize(6007);
    fileList.setAutoDelete(true);

    QVBoxLayout *layout = new QVBoxLayout(this, 5);
    stepLbl = new QLabel(i18n("Step 1: Generating image fingerprints."), this);
    layout->addWidget(stepLbl);
    layout->addSpacing(16);
    progress = new KProgress(Qt::Horizontal, this);
    progress->setValue(0);
    connect(this, SIGNAL(updateProgress(int)), progress,
            SLOT(setValue(int)));
    layout->addWidget(progress);
    layout->addSpacing(16);
    QWidget *btnFrame = new QWidget(this);
    QHBoxLayout *btnLayout = new QHBoxLayout(btnFrame);
    btnLayout->addStretch(1);
    QPushButton *cancelBtn = new QPushButton(i18n("Cancel"), btnFrame);
    connect(cancelBtn, SIGNAL(clicked()), this, SLOT(slotStopClicked()));
    btnLayout->addWidget(cancelBtn);
    btnLayout->addStretch(1);
    layout->addWidget(btnFrame);
    layout->addStretch(1);
    KStatusBar *statusBar = new KStatusBar(this);
    connect(this, SIGNAL(setStatusBarText(const QString &)), statusBar,
            SLOT(message(const QString &)));
    layout->addWidget(statusBar);

    setMinimumWidth(350);
    resize(sizeHint());

    show();

    view = NULL;
    stopProcessing = false;
    generateCompareData();
    if(!stopProcessing)
        runCompare();
}

void KIFCompare::closeEvent(QCloseEvent *)
{
    stopProcessing = true;
    //QSemiModal::closeEvent(ev);
}

void KIFCompare::generateCompareData()
{
    fileList.clear();
    modified = false;

    QDir dir(dirPath);

    // load existing db entries
    dbFile.setName(dir.absPath() + "/.pixiedupes");
    if(dbFile.open(IO_ReadOnly)){
        loadCompareDB();
        dbFile.close();
    }
    else
        qWarning("No DB file found in %s", dir.absPath().ascii());

    const QFileInfoList *fileList = dir.entryInfoList();
    QFileInfoListIterator it(*fileList);
    QFileInfo *fi;

    int percent = 0;
    int current = 1;
    int count = it.count();
    bool isImage;
    QImage img;

    // main loop
    while((fi = it.current()) && !stopProcessing){
        if(!fi->isDir()){
            KURL url("file:"+fi->absFilePath());
            isImage = KMimeType::findByURL(url, true, true)->name().left(6) ==
                "image/";
            if(isImage){
                loadCompareData(fi);
            }
        }
        if((int)(((float)current/count)*100) != percent){
            percent =  (int)(((float)current/count)*100);
            emit updateProgress(percent);
            kifapp()->processEvents();
        }
        ++it;
        ++current;
    }

    // save db if modified
    if(modified && !stopProcessing){
        if(!dbFile.open(IO_WriteOnly)){
            KMessageBox::sorry(NULL, i18n("You do not have write access to this folder!\n\
PixiePlus will be unable to store image data."),
i18n("Pixie Write Access Error"));
        }
        else{
            writeCompareDB();
            dbFile.close();
        }
    }
    else
        qWarning("No images modified or added. DB not written");

}

void KIFCompare::loadCompareData(QFileInfo *fi)
{
    // check if already in DB and updated
    KIFCompareData *data = fileList.find(fi->fileName().ascii());
    if(data){
        // is it updated?
        if(data->time >= fi->lastModified()){
            emit setStatusBarText(fi->fileName() + i18n(" already in DB"));
            kapp->processEvents();
            return;
        }
        else{
            emit setStatusBarText(fi->fileName() +
                                       i18n(" is in DB but modified..."));
            kapp->processEvents();
            fileList.remove(fi->fileName().ascii());
        }
    }
    else{
        emit setStatusBarText(i18n("Generating fingerprint for ") + fi->fileName() + "...");
        kapp->processEvents();
    }
    modified = true;

    QImage img;
    if(!loadImage(img, fi->absFilePath())){
        qWarning("Unable to load image: %s", fi->fileName().latin1());
        return;
    }
    //img = img.smoothScale(160, 160);
    img = KImageEffect::sample(img, 160, 160);
    KImageEffect::toGray(img);
    img = KImageEffect::blur(img, 99);
    KImageEffect::normalize(img);
    KImageEffect::equalize(img);
    //img = img.smoothScale(16, 16);
    img = KImageEffect::sample(img, 16, 16);
    KImageEffect::threshold(img, 128);
    img = img.convertDepth(1, MonoOnly | ThresholdDither);

    data = new KIFCompareData;
    data->time = fi->lastModified();
    // Can't just memcpy img.bits() because scanlines are aligned on a 32bit
    // boundary and there is only 2 bytes of data per scanline. Took me
    // awhile to figure this out ;-)
    int y, dy;
    unsigned char *scanline;
    for(y=0, dy=0; y < 16; ++y, dy+=2){
        scanline = (unsigned char *)img.scanLine(y);
        data->data[dy] = scanline[0];
        data->data[dy+1] = scanline[1];
    }

    fileList.insert(fi->fileName().ascii(), data);
}

void KIFCompare::loadCompareDB()
{
    emit setStatusBarText(i18n("Loading database..."));
    kapp->processEvents();
    QDataStream stream(&dbFile);
    QString fn;
    while(!dbFile.atEnd()){
        KIFCompareData *data = new KIFCompareData;
        stream >> fn;
        stream >> data->time;
        stream.readRawBytes((char *)data->data, 32);
        if(QFile::exists(dirPath + "/" + fn))
            fileList.insert(fn.ascii(), data);
        else
            qWarning("Ignoring invalid entry %s", fn.ascii());
    }
}

void KIFCompare::writeCompareDB()
{
    emit setStatusBarText(i18n("Saving database..."));
    kapp->processEvents();

    QDataStream stream(&dbFile);
    QAsciiDictIterator<KIFCompareData> it(fileList);
    for(it.toFirst(); it.current(); ++it){
        stream << QString(it.currentKey());
        stream << (*it).time;
        stream.writeRawBytes((const char *)(*it).data, 32);
    }
}

void KIFCompare::outputFingerPrint(const unsigned char *data)
{
    int i;
    QString s, str;
    for(i=0; i < 32; ++i){
        s.sprintf("%02x", data[i]);
        str += s;
    }
    qWarning("Generated fingerprint %s, len: %d", str.latin1(), str.length());
}

void KIFCompare::slotStopClicked()
{
    qWarning("In slotStopClicked");
    stopProcessing = true;
}

int KIFCompare::countBits(unsigned char val)
{
    unsigned char i=1;
    int acc = 0;
    int count;
    for(count=0; count < 8; ++count){
        if(val & i)
            ++acc;
        i = i << 1;
    }
    return(acc);
}

bool KIFCompare::checkIfMatched(const QString &first, const QString &second)
{
    KIFCompareViewItem *i = (KIFCompareViewItem *)view->firstChild();
    KIFCompareViewItem *j;
    bool exists = false;
    bool onematch = false;

    while(i && !exists){
        j = (KIFCompareViewItem *)i->firstChild();
        while(j && !exists){
            // check if a match from child to parent
            if((i->path() == first && j->path() == second) ||
               (j->path() == first && i->path() == second)){
                qWarning("Found previous parent to child match of %s to %s",
                         first.latin1(), second.latin1());
                exists = true;
            }
            // check if a match from child to child
            else if(j->path() == first || j->path() == second){
                if(onematch){
                    qWarning("Found previous child match for %s",
                             j->path().latin1());
                    exists = true;
                }
                else
                    onematch = true;
            }
            j = (KIFCompareViewItem *)j->nextSibling();
        }
        i = (KIFCompareViewItem *)i->nextSibling();
    }
    return(exists);
}

void KIFCompare::runCompare()
{
    emit updateProgress(0);
    stepLbl->setText(i18n("Step 2: Comparing fingerprints"));
    kapp->processEvents();

    QAsciiDictIterator<KIFCompareData> it(fileList);
    QAsciiDictIterator<KIFCompareData> secondIt(fileList);

    int percent = 0;
    int current = 1;
    int count = it.count();
    int bitcount;
    unsigned char xor_bits;
    int c;

    view = new KIFCompareView(dirPath, iSize);
    connect(view, SIGNAL(imageSelected(const QString &)), mgr,
            SLOT(slotAddAndSetURL(const QString &)));
    connect(view, SIGNAL(addToFileList(const QString &)), mgr,
            SLOT(slotAddURL(const QString &)));

    KIFCompareViewItem *parentItem;

    // todo - make option
    int threshold = 92;
    int maxbits = (int)(256.0*(1.0-threshold/100.0));
    QString str;

    for(it.toFirst(); it.current() && !stopProcessing; ++it){
        emit setStatusBarText(i18n("Searching for matches to ") + it.currentKey());
        // check this item to every other item
        parentItem = NULL;
        for(secondIt.toFirst(); secondIt.current() && !stopProcessing; ++secondIt){
            // xor and accumulate bits
            if((*secondIt).data != (*it).data){
                bitcount = 0;
                for(c=0; c < 32; ++c){
                    xor_bits = (*it).data[c] ^ (*secondIt).data[c];
                    if(xor_bits)
                        bitcount += countBits(xor_bits);
                }
                if(bitcount <= maxbits){
                    // match found
                    if(!checkIfMatched( dirPath+"/"+it.currentKey(),
                                        dirPath+"/"+secondIt.currentKey())){
                        if(!parentItem){
                            parentItem = new KIFCompareViewItem(view,
                                                                dirPath+"/"+it.currentKey(),
                                                                iSize);
                        }
                        (void)new KIFCompareViewItem(parentItem,
                                                     dirPath+"/"+secondIt.currentKey(),
                                                     bitcount, iSize);
                    }
                }
            }
        }
        if((int)(((float)current/count)*100) != percent){
            percent =  (int)(((float)current/count)*100);
            emit updateProgress(percent);
            kifapp()->processEvents();
        }
        ++it;
        ++current;
    }
    if(stopProcessing){
        qWarning("runCompare canceled.");
        delete view;
        view = NULL;
        return;
    }

    QListViewItem *vi = view->firstChild();
    while(vi){
        vi->setOpen(true);
        vi = vi->nextSibling();
    }
    qWarning("Time elapsed: %d", testTime.elapsed());

    view->show();
}


KIFCompareView::KIFCompareView(const QString &path, int iconSize,
                               const char *name)
    : QListView(NULL, name, WDestructiveClose)
{
    dirWatch = new KDirWatch;
    connect(dirWatch, SIGNAL(dirty(const QString &)), this,
            SLOT(slotDirChanged(const QString &)));
    dirWatch->addDir(path);
    dirWatch->startScan();
    setAllColumnsShowFocus(true);
    setTreeStepSize(64);
    setShowToolTips(false);
    addColumn(i18n("Image"));
    addColumn(i18n("Details"));
    setCaption(i18n("Comparison Results"));
    connect(this, SIGNAL(doubleClicked(QListViewItem *)), this,
            SLOT(slotDoubleClicked(QListViewItem *)));
    connect(this, SIGNAL(rightButtonClicked(QListViewItem *, const QPoint &, int)), this,
            SLOT(slotRightButton(QListViewItem *, const QPoint &, int)));
    QImage tmpImg(iconSize-4, iconSize-4, 32);
    tmpImg.fill(Qt::lightGray.rgb());
    QImage dest;
    KIFBorderEffect::solid(tmpImg, dest, Qt::black, 2);
    tmpPix.convertFromImage(dest);

    tmpImg.reset();
    tmpImg.create(iconSize-4, iconSize-4, 32);
    tmpImg.fill(Qt::darkGray.rgb());
    KIFBorderEffect::solid(tmpImg, dest, Qt::black, 2);
    emptyPix.convertFromImage(dest);

    parentCg = colorGroup();
    parentCg.setColor(QColorGroup::Base, parentCg.base().dark(110));
    stopProcessing = false;
    tips = new CompareTip(this, NULL);
}

KIFCompareView::~KIFCompareView()
{
    qWarning("In KIFCompareView destructor");
    delete dirWatch;
    delete tips;
}

void KIFCompareView::makeThumbnails()
{
    QListViewItem *i, *j;

    i = firstChild();
    while(i && !stopProcessing){
        ((KIFCompareViewItem *)i)->calcPixmap();
        kapp->processEvents();
        j = i->firstChild();
        while(j && !stopProcessing){
            ((KIFCompareViewItem*)j)->calcPixmap();
            kapp->processEvents();
            j = j->nextSibling();
        }
        i = i->nextSibling();
    }
}

void KIFCompareView::closeEvent(QCloseEvent *ev)
{
    stopProcessing = true;
    QListView::closeEvent(ev);
}

void KIFCompareView::slotDirChanged(const QString &)
{
    qWarning("Compare view folder changed");

    // iterate through all items and see if anything was deleted
    KIFCompareViewItem *i, *j, *tmp;
    bool changed = false;
    QFileInfo fi;
    i = (KIFCompareViewItem *)firstChild();
    while(i){
        if(!QFile::exists(i->path())){
            // Parent item deleted. TODO: Rescan list and remove item, moving
            // child item to top if more than 2.
            changed = true;
            i->setPath(QString::null);
            i->setPixmap(0, emptyPix);
            i->setText(1, i18n("Original Image Deleted"));
        }
        j = (KIFCompareViewItem *)i->firstChild();
        while(j){
            tmp = (KIFCompareViewItem *)j->nextSibling();
            if(!QFile::exists(j->path())){
                // Child item deleted.
                changed = true;
                delete j;
            }
            j = tmp;
        }
        i = (KIFCompareViewItem *)i->nextSibling();
    }
    if(changed){
        qWarning("File was deleted");
        // see if any parent items are empty, or if deleted parent items only
        // had one match
        i = (KIFCompareViewItem *)firstChild();
        while(i){
            tmp = (KIFCompareViewItem *)i->nextSibling();
            if(i->childCount() == 0 ||
               (i->path() == QString::null && i->childCount() == 1))
                delete i;
            i = tmp;
        }
    }
}

void KIFCompareView::slotRightButton(QListViewItem *i, const QPoint &p, int col)
{
    if(!i || col == -1 || ((KIFCompareViewItem *)i)->path() == QString::null)
        return;
    KPopupMenu *mnu = new KPopupMenu;
    mnu->insertTitle(BarIcon("filenew", KIcon::SizeSmall),
                     i18n("Duplicate Image"));
    mnu->insertItem(BarIcon("filenew", KIcon::SizeSmall),
                    i18n("Copy to FileList"), 1);
    mnu->insertItem(BarIcon("editcopy", KIcon::SizeSmall),
                    i18n("Copy file location"), 2);
    mnu->insertItem(BarIcon("editcopy", KIcon::SizeSmall),
                   i18n("Copy filename only"), 3);
    mnu->insertSeparator();
    mnu->insertItem(BarIcon("edittrash", KIcon::SizeSmall),
                    i18n("Delete"), 4);
    int id = mnu->exec(p);
    delete mnu;
    if(id == -1)
        return;
    if(id == 1){
        emit addToFileList(((KIFCompareViewItem *)i)->path());
    }
    else if(id == 2){
        QFileInfo fi(((KIFCompareViewItem *)i)->path());
#if QT_VERSION & 0x00FF00
        QApplication::clipboard()->setText(fi.absFilePath(),
                                           QClipboard::Selection);
#else
        QApplication::clipboard()->setText(fi.absFilePath());
#endif
    }
    else if(id == 3){
        QFileInfo fi(((KIFCompareViewItem *)i)->path());
#if QT_VERSION & 0x00FF00
        QApplication::clipboard()->setText(fi.fileName(),
                                           QClipboard::Selection);
#else
        QApplication::clipboard()->setText(fi.fileName());
#endif
    }
    else if(id == 4){
        QString fn = ((KIFCompareViewItem *)i)->path();
        if(unlink(fn.ascii()) == -1){
            KMessageBox::sorry(this, i18n("Could not remove file ") + fn.ascii());
            return;
        }
    }
}

void KIFCompareView::slotDoubleClicked(QListViewItem *item)
{
    if(((KIFCompareViewItem *)item)->path() != QString::null)
        emit imageSelected(((KIFCompareViewItem *)item)->path());
}

KIFCompareViewItem::KIFCompareViewItem(KIFCompareView *parent, const QString &path,
                                       int iconSize)
    : QListViewItem(parent)
{
    pathStr = path;
    iSize = iconSize;
    QFileInfo fi(path);
    QString imageStr, cameraStr, commentStr;

    QString str = i18n("Original image:\n") + fi.fileName() + "\n" +
        calcSizeString(fi.size());
    appendTooltipData(QFile::encodeName(fi.absFilePath()),
                      imageStr, cameraStr, commentStr, false);
    if(!imageStr.isEmpty())
        str += "\n" + imageStr;

    //calcPixmap(fi, iconSize);
    setPixmap(0, parent->tempFill());
    setText(1, str);
    bits = 0; // shouldn't be used
}

KIFCompareViewItem::KIFCompareViewItem(KIFCompareViewItem *parent, const QString &path,
                                       int bitcount, int iconSize)
    : QListViewItem(parent)
{
    QString imageStr, cameraStr, commentStr;
    pathStr = path;
    iSize = iconSize;
    QFileInfo fi(path);
    QString str;
    str.sprintf("%0.2f%% match\n", (1.0-bitcount/256.0)*100.0);
    str += fi.fileName() + "\n" + calcSizeString(fi.size());
    appendTooltipData(QFile::encodeName(fi.absFilePath()),
                      imageStr, cameraStr, commentStr, false);
    if(!imageStr.isEmpty())
        str += "\n" + imageStr;

    //calcPixmap(fi, iconSize);
    setPixmap(0, ((KIFCompareView*)listView())->tempFill());
    setText(1, str);
    bits = bitcount;
}

void KIFCompareViewItem::setup()
{
    QFont fnt(listView()->font());
    QFontMetrics fm(fnt);
    if(fm.lineSpacing()*7 > iSize+2)
        setHeight(fm.lineSpacing()*7);
    else
        setHeight(iSize+2);
}

void KIFCompareViewItem::setOpen(bool open)
{
    if(open)
        QListViewItem::setOpen(open);
}

void KIFCompareViewItem::calcPixmap()
{
    QFileInfo fi(pathStr);
    qWarning("Calculating pixmap for %s", fi.fileName().latin1());
    QImage img;
    switch(iSize){
    case 112:
        if(QFile::exists(fi.dirPath(true) + "/.pics/huge/" + fi.fileName()))
            img.load(fi.dirPath(true) + "/.pics/huge/" + fi.fileName(), "PNG");
        break;
    case 90:
        if(QFile::exists(fi.dirPath(true) + "/.pics/large/" + fi.fileName()))
            img.load(fi.dirPath(true) + "/.pics/large/" + fi.fileName(), "PNG");
        break;
    case 64:
        if(QFile::exists(fi.dirPath(true) + "/.pics/med/" + fi.fileName()))
            img.load(fi.dirPath(true) + "/.pics/med/" + fi.fileName(), "PNG");
        break;
    case 48:
        if(QFile::exists(fi.dirPath(true) + "/.pics/small/" + fi.fileName()))
            img.load(fi.dirPath(true) + "/.pics/small/" + fi.fileName(), "PNG");
        break;
    default:
        break;
    }
    if(img.isNull()){
        if(!loadImage(img, fi.absFilePath())){
            setText(1, i18n("Invalid Image!"));
            return;
        }
        else if(img.width() > iSize || img.height() > iSize){
            if(img.width() > img.height()){
                float percent = (((float)iSize)/img.width());
                int h = (int)(img.height()*percent);
                img = img.smoothScale(iSize, h);
            }
            else{
                float percent = (((float)iSize)/img.height());
                int w = (int)(img.width()*percent);
                img = img.smoothScale(w, iSize);
            }
        }
    }
    QPixmap pix;
    pix.convertFromImage(img);
    setPixmap(0, pix);
}

QString KIFCompareViewItem::key(int, bool) const
{
    return(QString::number(bits));
}

QString KIFCompareViewItem::calcSizeString(int size)
{
    QString str;
    if(size >= 1024){
        size /= 1024;
        if(size >= 1024){
            size /= 1024;
            str += i18n("Size: ")+QString::number(size)+"M";
        }
        else{
            str += i18n("Size: ")+QString::number(size)+"K";

        }
    }
    else{
        str += i18n("Size: ")+QString::number(size)+"B";
    }
    return(str);
}

void KIFCompareViewItem::paintCell(QPainter *p, const QColorGroup &cg,
                                   int col, int w, int align)
{
    if(!childCount())
        QListViewItem::paintCell(p, cg, col, w, align);
    else
        QListViewItem::paintCell(p, ((KIFCompareView *)listView())->parentColorGroup(),
                                 col, w, align);
}

void KIFCompareViewItem::paintBranches(QPainter *p, const QColorGroup &cg,
                                       int w, int /*y*/, int h)
{
    p->fillRect(QRect(0, 0, w, h ), cg.base());
}

void CompareTip::maybeTip(const QPoint &p)
{
    KIFCompareView *view = (KIFCompareView *)parentWidget();
    KIFCompareViewItem *item = (KIFCompareViewItem *)view->itemAt(p);
    if(item){
        QString tipStr;
        QFileInfo fi(item->path());
        tipStr += fi.fileName() + "\n" + i18n("Dbl click to view, right click for options");
        tip(view->itemRect(item), tipStr);
    }
}

