#include "imageringlist.h"
#include <functional>
#include <QThreadPool>
#include <QDir>
#include <QSettings>
#include <QTimer>
#include <QRegularExpression>
#include "loadrunable.h"
#include "rawimage.h"
#include "database.h"

using namespace std;

int DEFAULT_WIDTH = 2;

Image::Image(const QString name, int number, ImageRingList *ringList) :
    m_loading(false),
    m_released(true),
    m_current(false),
    m_number(number),
    m_name(name),
    m_ringList(ringList)
{
}

void Image::load(int index, QThreadPool *pool)
{
    if(index != m_info.index && !m_loading)
        m_rawImage.reset();

    if(!m_rawImage && !m_loading)
    {
        m_loading = true;
        m_released = false;
        pool->start(new LoadRunable(m_name, this, m_ringList->analyzeLevel(), index));
    }
    if(!m_loading && m_rawImage)
        emit pixmapLoaded(this);
}

void Image::loadThumbnail(QThreadPool *pool)
{
    if(!m_thumbnail)
        pool->start(new LoadRunable(m_name, this, AnalyzeLevel::None, 0, true));
    else
        emit thumbnailLoaded(this);
}

void Image::release()
{
    m_rawImage.reset();
    m_released = true;
    m_loading = false;
}

QString Image::name() const
{
    return m_name;
}

std::shared_ptr<RawImage> Image::rawImage()
{
    return m_rawImage;
}

const RawImage *Image::thumbnail() const
{
    return m_thumbnail.get();
}

ImageInfoData Image::info() const
{
    return m_info;
}

bool Image::isCurrent() const
{
    return !m_released;
}

int Image::number() const
{
    return m_number;
}

void Image::clearThumbnail()
{
    m_thumbnail.reset();
}

bool Image::isLoading() const
{
    return m_loading;
}

void Image::imageLoaded(std::shared_ptr<RawImage> rawImage, ImageInfoData info)
{
    m_loading = false;
    if(!m_released)
    {
        m_rawImage = rawImage;
        m_info = info;
        emit pixmapLoaded(this);
    }
}

void Image::thumbnailLoadFinish(std::shared_ptr<RawImage> rawImage)
{
    m_thumbnail = rawImage;
    if(m_thumbnail)
        emit thumbnailLoaded(this);
}

ImageRingList::ImageRingList(Database *database, const QStringList &nameFilter, QObject *parent) : QAbstractListModel(parent)
  , m_liveMode(false)
  , m_analyzeLevel(None)
  , m_database(database)
  , m_nameFilter(nameFilter)
  , m_fileSuffix(nameFilter)
{
    connect(&m_fileSystemWatcher, &QFileSystemWatcher::directoryChanged, this, &ImageRingList::dirChanged);
    m_nameFilter.replaceInStrings(QRegularExpression("^"), "*.");
    m_loadPool = new QThreadPool(this);
    m_loadPool->setThreadPriority(QThread::LowPriority);
    m_thumbPool = new QThreadPool(this);
    m_thumbPool->setThreadPriority(QThread::LowPriority);

    m_slideShowTimer = new QTimer(this);
    connect(m_slideShowTimer, &QTimer::timeout, this, static_cast<void (ImageRingList::*)()>(&ImageRingList::increment));

    m_dirChangeDelay = new QTimer(this);
    m_dirChangeDelay->setInterval(3000);
    m_dirChangeDelay->setSingleShot(true);
    connect(m_dirChangeDelay, &QTimer::timeout, this, &ImageRingList::reloadDir);
}

ImageRingList::~ImageRingList()
{
    m_loadPool->clear();
    m_thumbPool->clear();

    m_loadPool->waitForDone();
    m_thumbPool->waitForDone();
}

bool ImageRingList::setDir(const QString path, const QString &currentFile, bool recursive)
{
    QDir dir(path);

    if(dir.exists())
    {
        m_currentDir = path;
        QStringList scannedDirs;
        QStringList absolutePaths;
        std::function<void(const QString&)> scanDir = [&](const QString &path)
        {
            QDir dir(path);
            if(scannedDirs.contains(dir.canonicalPath()))return;
            scannedDirs.append(dir.canonicalPath());
            QDir::SortFlags sortFlags = m_liveMode ? QDir::Time : m_sort | QDir::IgnoreCase;
            if(m_reversed)sortFlags |= QDir::Reversed;

            if(recursive)
            {
                QStringList dirs = dir.entryList(QDir::Readable | QDir::Dirs | QDir::NoDotAndDotDot, sortFlags);
                for(const QString &subdir : dirs)
                    scanDir(dir.absoluteFilePath(subdir));
            }

            QStringList list = dir.entryList(m_nameFilter, QDir::Files | QDir::Readable, sortFlags);
            for(const QString &file : list)
            {
                absolutePaths.append(dir.absoluteFilePath(file));
            }
        };

        scanDir(path);
        //qDebug() << absolutePaths.size();
        setFilesPrivate(absolutePaths, m_liveMode ? absolutePaths.first() : currentFile);

        if(m_fileSystemWatcher.directories().size())
            m_fileSystemWatcher.removePaths(m_fileSystemWatcher.directories());
        m_fileSystemWatcher.addPath(path);
        return true;
    }
    return false;
}

void ImageRingList::setFile(const QString &file)
{
    if(!file.isEmpty())
    {
        QFileInfo info(file);
        if(info.isDir())
            setDir(file, QString(), true);
        else
            setDir(info.absolutePath(), file);
    }
}

void ImageRingList::setFiles(QStringList files)
{
    QRegularExpression reg("(" + m_fileSuffix.join("|") + ")");
    files.removeIf([&reg](const QString &file){
        QFileInfo info(file);
        auto match = reg.match(info.suffix());
        return !match.hasMatch() || !info.exists() || !info.isReadable() || !info.isFile();
    });
    setFilesPrivate(files);
}

ImagePtr ImageRingList::currentImage()
{
    if(m_images.size())
        return *m_currImage;
    else
        return 0;
}

QString ImageRingList::currentDir() const
{
    return m_currentDir;
}

void ImageRingList::increment()
{
    if(m_images.size())
    {
        //don't increment if current image was not loaded yet
        if((*m_currImage)->isLoading())
            return;

        (*m_firstImage)->release();
        m_firstImage = increment(m_firstImage);
        m_currImage = increment(m_currImage);
        (*m_currImage)->load(0, m_loadPool);
        m_lastImage = increment(m_lastImage);
        (*m_lastImage)->load(0, m_loadPool);
    }
}

void ImageRingList::decrement()
{
    if(m_images.size())
    {
        //don't decrement if current image was not loaded yet
        if((*m_currImage)->isLoading())
            return;

        (*m_lastImage)->release();
        m_firstImage = decrement(m_firstImage);
        m_currImage = decrement(m_currImage);
        (*m_currImage)->load(0, m_loadPool);
        m_lastImage = decrement(m_lastImage);
        (*m_firstImage)->load(0, m_loadPool);
    }
}

void ImageRingList::prevSubImage()
{
    if(m_images.size())
    {
        if((*m_currImage)->isLoading())
            return;

        int index = (*m_currImage)->info().index;
        int num = (*m_currImage)->info().num;
        if(num > 1)
            (*m_currImage)->load(index == 0 ? num - 1 : index - 1, m_loadPool);
    }
}

void ImageRingList::nextSubImage()
{
    if(m_images.size())
    {
        if((*m_currImage)->isLoading())
            return;

        int index = (*m_currImage)->info().index;
        int num = (*m_currImage)->info().num;
        if(num > 1)
            (*m_currImage)->load((index + 1) % num, m_loadPool);
    }
}

void ImageRingList::setMarked()
{
    QStringList files = m_database->getMarkedFiles();
    files.removeIf([](const QString &file){
        QFileInfo info(file);
        return !info.exists() || !info.isReadable();
    });
    setFilesPrivate(files);
}

void ImageRingList::reloadImage()
{
    if(*m_currImage)
    {
        int index = (*m_currImage)->info().index;
        (*m_currImage)->release();
        (*m_currImage)->load(index, m_loadPool);
    }
}

void ImageRingList::setLiveMode(bool live)
{
    m_liveMode = live;
}

void ImageRingList::setCalculateStats(bool stats)
{
    m_analyzeLevel = stats ? Statistics : None;
}

void ImageRingList::setFindPeaks(bool findPeaks)
{
    m_analyzeLevel = findPeaks ? Peaks : None;
}

void ImageRingList::setFindStars(bool findStars)
{
    m_analyzeLevel = findStars ? Stars : None;
}

AnalyzeLevel ImageRingList::analyzeLevel() const
{
    return m_analyzeLevel;
}

void ImageRingList::loadFile(int row)
{
    if(row < m_images.size())
    {
        int diff = m_currImage != m_images.end() ? row - (m_currImage - m_images.begin()) : -100;
        if(diff == 1)
            increment();
        else if(diff == -1)
            decrement();
        else
        {
            m_firstImage = m_currImage = m_lastImage = m_images.begin()+row;
            if(m_images.empty())
                return;

            (*m_currImage)->load(0, m_loadPool);

            m_width = DEFAULT_WIDTH<m_images.size()/2 ? DEFAULT_WIDTH : m_images.size()/2;
            if(m_liveMode)
                m_width = 0;

            for(int i=0; i<m_width; i++)
            {
                m_firstImage = decrement(m_firstImage);
                (*m_firstImage)->load(0, m_loadPool);
                m_lastImage = increment(m_lastImage);
                (*m_lastImage)->load(0, m_loadPool);
            }
            if(m_lastImage != m_firstImage)
            {
                QList<ImagePtr>::iterator iter = increment(m_lastImage);
                while(m_firstImage != iter)
                {
                    (*iter)->release();
                    iter = increment(iter);
                }
            }
        }
    }
}

void ImageRingList::loadThumbnails()
{
    for(auto &img : m_images)
    {
        img->loadThumbnail(m_thumbPool);
    }
}

void ImageRingList::stopLoading()
{
    m_thumbPool->clear();
    m_thumbPool->waitForDone();
}

int ImageRingList::imageCount() const
{
    return m_images.size();
}

QStringList ImageRingList::imageNames() const
{
    QStringList ret;
    for(auto &img : m_images)
        ret.push_back(img->name());
    return ret;
}

void ImageRingList::updateMark()
{
    if(m_images.size())
    {
        QModelIndex idx = index(m_currImage - m_images.begin(), 0);
        emit dataChanged(idx, idx, {Qt::FontRole});
    }
}

void ImageRingList::clearThumbnails()
{
    for(auto &img : m_images)
        img->clearThumbnail();
}

/*QModelIndex ImageRingList::index(int row, int column, const QModelIndex &parent) const
{
    Q_UNUSED(parent);
    return createIndex(row, column, m_images.at(row).get());
}

QModelIndex ImageRingList::parent(const QModelIndex &child) const
{
    Q_UNUSED(child);
    return QModelIndex();
}*/

int ImageRingList::rowCount(const QModelIndex &parent) const
{
    if(parent == QModelIndex())
        return m_images.size();
    else
        return 0;
}

/*int ImageRingList::columnCount(const QModelIndex &parent) const
{
    Q_UNUSED(parent);
    return 1;
}*/

QVariant ImageRingList::data(const QModelIndex &index, int role) const
{
    if(index.isValid() && index.row() >= 0 && index.row() < m_images.size())
    {
        switch(role)
        {
        case Qt::DisplayRole:
        {
            QFileInfo info(m_images.at(index.row())->name());
            return info.fileName();
        }
        case Qt::FontRole:
        {
            bool marked = m_database->isMarked(m_images.at(index.row())->name());
            QFont font;
            font.setBold(marked);
            return font;
        }
        default:
            return QVariant();
        }
    }
    return QVariant();
}

QVariant ImageRingList::headerData(int section, Qt::Orientation orientation, int role) const
{
    if(section==0 && orientation==Qt::Horizontal && role==Qt::DisplayRole)
    {
        return tr("Name");
    }
    return QVariant();
}

void ImageRingList::setPreload(int width)
{
    DEFAULT_WIDTH = width;
    if(m_images.size() == 0)return;

    int newWidth = DEFAULT_WIDTH<m_images.size()/2 ? DEFAULT_WIDTH : m_images.size()/2;
    if(newWidth > m_width)
    {
        for(int i = newWidth - m_width; i>0; i--)
        {
            m_firstImage = decrement(m_firstImage);
            (*m_firstImage)->load(0, m_loadPool);
            m_lastImage = increment(m_lastImage);
            (*m_lastImage)->load(0, m_loadPool);
        }
    }
    if(newWidth < m_width)
    {
        for(int i = m_width - newWidth; i>0; i--)
        {
            (*m_firstImage)->release();
            m_firstImage = increment(m_firstImage);
            (*m_lastImage)->release();
            m_lastImage = decrement(m_lastImage);
        }
    }
    m_width = newWidth;
}

void ImageRingList::setSort(QDir::SortFlag sort)
{
    if(m_sort != sort)
    {
        m_sort = sort;
        if(m_images.size())
        {
            QString path = (*m_currImage)->name();
            setFile(path);
        }
    }
}

void ImageRingList::reverseSort()
{
    m_reversed = !m_reversed;
    if(m_images.size())
    {
        QString path = (*m_currImage)->name();
        setFile(path);
    }
}

void ImageRingList::toggleSlideshow(bool start)
{
    if(start)
    {
        QSettings settings;
        int time = settings.value("settings/slideshowtime", 1.0).toDouble() * 1000;
        m_slideShowTimer->start(time);
    }
    else
    {
        m_slideShowTimer->stop();
    }
}

void ImageRingList::setFilesPrivate(const QStringList files, const QString &currentFile)
{
    m_loadPool->clear();
    m_thumbPool->clear();
    m_loadPool->waitForDone();
    m_thumbPool->waitForDone();
    beginResetModel();
    m_images.clear();
    int i = 0;
    for(const QString &file : files)
    {
        ImagePtr ptr = make_shared<Image>(file, i++, this);
        connect(ptr.get(), &Image::pixmapLoaded, this, &ImageRingList::imageLoaded);
        connect(ptr.get(), &Image::thumbnailLoaded, this, &ImageRingList::thumbnailLoaded);
        m_images.append(ptr);
    }

    int index = files.indexOf(currentFile);
    if(index < 0)
        index = 0;

    endResetModel();
    m_currImage = m_images.end();
    loadFile(index);
}

QList<ImagePtr>::iterator ImageRingList::increment(QList<ImagePtr>::iterator iter)
{
    iter++;
    if(iter == m_images.end())
        iter = m_images.begin();
    return iter;
}

QList<ImagePtr>::iterator ImageRingList::decrement(QList<ImagePtr>::iterator iter)
{
    if(iter == m_images.begin())
        iter = m_images.end();
    iter--;
    return iter;
}

void ImageRingList::imageLoaded(Image *image)
{
    if(image->name() == (*m_currImage)->name())
    {
        emit pixmapLoaded(image);
        emit infoLoaded(image->info());
        emit currentImageChanged(m_currImage-m_images.begin());
    }
}

void ImageRingList::dirChanged(QString)
{
    if(m_liveMode)
        reloadDir();
    else
        m_dirChangeDelay->start();
}

void ImageRingList::reloadDir()
{
    QString currentFile;

    if(m_images.size())
        currentFile = (*m_currImage)->name();

    setDir(m_currentDir, currentFile);
    if(m_images.size())
        emit currentImageChanged(m_currImage-m_images.begin());
}
