/***************************************************************************
 *   Copyright (C) 2009-2024 by Ilya Kotov                                 *
 *   forkotov02@ya.ru                                                      *
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 *   This program is distributed in the hope that it will be useful,       *
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
 *   GNU General Public License for more details.                          *
 *                                                                         *
 *   You should have received a copy of the GNU General Public License     *
 *   along with this program; if not, write to the                         *
 *   Free Software Foundation, Inc.,                                       *
 *   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.         *
 ***************************************************************************/

#include <QFile>
#include <QFileInfo>
#include <QBuffer>
#include <QMutexLocker>
#include <QCoreApplication>
#include <QImageReader>
#include "decoder.h"
#include "decoderfactory.h"
#include "abstractengine.h"
#include "inputsource.h"
#include "qmmpsettings.h"
#include "metadatamanager.h"

#define COVER_CACHE_SIZE 10

MetaDataManager* MetaDataManager::m_instance = nullptr;

MetaDataManager::MetaDataManager() : m_settings(QmmpSettings::instance())
{}

MetaDataManager::~MetaDataManager()
{
    clearCoverCache();
}

QList<TrackInfo *> MetaDataManager::createPlayList(const QString &path, TrackInfo::Parts parts, QStringList *ignoredPaths) const
{
    QList<TrackInfo *> list;
    DecoderFactory *fact = nullptr;
    EngineFactory *efact = nullptr;
    QStringList dummyList;
    if(!ignoredPaths)
        ignoredPaths = &dummyList;

    if(!path.contains(u"://"_s)) //local file
    {
        if(!QFile::exists(path))
            return list;

        if(!(fact = Decoder::findByFilePath(path, m_settings->determineFileTypeByContent())))
            efact = AbstractEngine::findByFilePath(path);
    }
    else
    {
        QString scheme = path.section(u"://"_s, 0, 0);
        if(InputSource::findByUrl(path))
        {
            list << new TrackInfo(path);
        }
        else
        {
            for(DecoderFactory *f : Decoder::factories())
            {
                if(f->properties().protocols.contains(scheme) && Decoder::isEnabled(f))
                {
                    fact = f;
                    break;
                }
            }
        }
    }

    if(fact)
        list = fact->createPlayList(path, parts, ignoredPaths);
    else if(efact)
        list = efact->createPlayList(path, parts, ignoredPaths);

    for(TrackInfo *info : std::as_const(list))
    {
        if(info->value(Qmmp::DECODER).isEmpty() && (fact || efact))
            info->setValue(Qmmp::DECODER, fact ? fact->properties().shortName : efact->properties().shortName);
        if(info->value(Qmmp::FILE_SIZE).isEmpty() && !path.contains(u"://"_s))
            info->setValue(Qmmp::FILE_SIZE, QFileInfo(path).size());
    }
    return list;
}

MetaDataModel* MetaDataManager::createMetaDataModel(const QString &path, bool readOnly) const
{
    DecoderFactory *fact = nullptr;
    EngineFactory *efact = nullptr;

    if (!path.contains(u"://"_s)) //local file
    {
        if(!QFile::exists(path))
            return nullptr;
        if((fact = Decoder::findByFilePath(path, m_settings->determineFileTypeByContent())))
            return fact->createMetaDataModel(path, readOnly);
        if((efact = AbstractEngine::findByFilePath(path)))
            return efact->createMetaDataModel(path, readOnly);
        return nullptr;
    }

    QString scheme = path.section(u"://"_s, 0, 0);
    MetaDataModel *model = nullptr;
    if((fact = Decoder::findByProtocol(scheme)))
    {
        return fact->createMetaDataModel(path, readOnly);
    }
    for(EngineFactory *efact : AbstractEngine::enabledFactories())
    {
        if(efact->properties().protocols.contains(scheme))
            model = efact->createMetaDataModel(path, readOnly);
        if(model)
            return model;
    }
    return nullptr;
}

QStringList MetaDataManager::filters() const
{
    QStringList filters;
    for(const DecoderFactory *fact : Decoder::enabledFactories())
    {
        if (!fact->properties().filters.isEmpty())
            filters << QStringLiteral("%1 (%2)").arg(fact->properties().description, fact->properties().filters.join(QChar::Space));
    }
    for(const EngineFactory *fact : AbstractEngine::enabledFactories())
    {
        if (!fact->properties().filters.isEmpty())
            filters << QStringLiteral("%1 (%2)").arg(fact->properties().description, fact->properties().filters.join(QChar::Space));
    }
    return filters;
}

QStringList MetaDataManager::nameFilters() const
{
    QStringList filters = Decoder::nameFilters();
    filters << AbstractEngine::nameFilters();
    if(m_settings->determineFileTypeByContent())
        filters << u"*"_s;
    filters.removeDuplicates();
    return filters;
}

QStringList MetaDataManager::protocols() const
{
    QStringList p;
    p << InputSource::protocols();
    p << Decoder::protocols();
    p << AbstractEngine::protocols();
    p.removeDuplicates();
    return p;
}

QList<QRegularExpression> MetaDataManager::regExps() const
{
    return InputSource::regExps();
}

bool MetaDataManager::supports(const QString &fileName) const
{
    if (!fileName.contains(u"://"_s)) //local file
    {
        if(!QFile::exists(fileName))
            return false;
        if(Decoder::findByFilePath(fileName))
            return true;
        if(AbstractEngine::findByFilePath(fileName))
            return true;
        return false;
    }
    return false;
}

QImage MetaDataManager::getCover(const QString &url) const
{
    QMutexLocker locker(&m_mutex);
    for(int i = 0; i < m_cover_cache.size(); ++i)
    {
        if(m_cover_cache[i]->url == url)
            return m_cover_cache[i]->coverImage;
    }

    m_cover_cache << createCoverCacheItem(url);

    while(m_cover_cache.size() > COVER_CACHE_SIZE)
        delete m_cover_cache.takeFirst();

    return m_cover_cache.last()->coverImage;
}

QString MetaDataManager::getCoverPath(const QString &url) const
{
    QMutexLocker locker(&m_mutex);
    for(int i = 0; i < m_cover_cache.size(); ++i)
    {
        if(m_cover_cache[i]->url == url)
            return m_cover_cache[i]->coverPath;
    }

    m_cover_cache << createCoverCacheItem(url);

    while(m_cover_cache.size() > COVER_CACHE_SIZE)
        delete m_cover_cache.takeFirst();

    return m_cover_cache.last()->coverPath;
}

QString MetaDataManager::findCoverFile(const QString &fileName) const
{
    if(!m_settings->useCoverFiles())
        return QString();

    if(!QFile::exists(fileName))
    {
        return QString();
    }

    QFileInfoList l = findCoverFiles(QFileInfo(fileName).absoluteDir(), m_settings->coverSearchDepth());
    return l.isEmpty() ? QString() : l.at(0).filePath();
}

QFileInfoList MetaDataManager::findCoverFiles(QDir dir, int depth) const
{
    dir.setFilter(QDir::Files | QDir::Hidden);
    dir.setSorting(QDir::Name);
    QFileInfoList file_list = dir.entryInfoList(m_settings->coverNameFilters());

    const auto fileListCopy = file_list; //avoid container modification
    for(const QFileInfo &i : std::as_const(fileListCopy))
    {
        if(QDir::match(m_settings->coverNameFilters(false), i.fileName()))
            file_list.removeAll(i);

        if(QImageReader::imageFormat(i.filePath()).isEmpty()) //remove unsupported image formats
            file_list.removeAll(i);
    }
    if(!depth || !file_list.isEmpty())
        return file_list;

    depth--;
    dir.setFilter(QDir::Dirs | QDir::NoDotAndDotDot);
    dir.setSorting(QDir::Name);
    const QFileInfoList dir_info_list = dir.entryInfoList();
    for(const QFileInfo &i : std::as_const(dir_info_list))
    {
        file_list << findCoverFiles(QDir(i.absoluteFilePath()), depth);
    }
    return file_list;
}

MetaDataManager::CoverCacheItem *MetaDataManager::createCoverCacheItem(const QString &url) const
{
    CoverCacheItem *item = new CoverCacheItem;
    item->url = url;
    if(!url.contains(u"://"_s) && m_settings->useCoverFiles())
        item->coverPath = findCoverFile(url);

    if(item->coverPath.isEmpty())
    {
        MetaDataModel *model = createMetaDataModel(url, true);
        if(model)
        {
            item->coverPath = model->coverPath();
            item->coverImage = model->cover();
            delete model;
        }
    }

    if(!item->coverPath.isEmpty() && item->coverImage.isNull())
        item->coverImage = QImage(item->coverPath);

    if(item->coverImage.width() > 1024 || item->coverImage.height() > 1024)
        item->coverImage = item->coverImage.scaled(1024, 1024, Qt::KeepAspectRatio, Qt::SmoothTransformation);

    return item;
}

void MetaDataManager::clearCoverCache()
{
    QMutexLocker locker(&m_mutex);
    qDeleteAll(m_cover_cache);
    m_cover_cache.clear();
}

void MetaDataManager::prepareForAnotherThread()
{
    //this hack should load all required plugins
    InputSource::enabledFactories();
    Decoder::enabledFactories();
    AbstractEngine::enabledFactories();
}

bool MetaDataManager::hasMatch(const QList<QRegularExpression> &regExps, const QString &path)
{
    for(const QRegularExpression &re : std::as_const(regExps))
    {
        if(re.match(path).hasMatch())
            return true;
    }
    return false;
}

MetaDataManager *MetaDataManager::instance()
{
    if(!m_instance)
    {
        m_instance = new MetaDataManager();
        qAddPostRoutine(&MetaDataManager::destroy);
    }
    return m_instance;
}

void MetaDataManager::destroy()
{
    delete m_instance;
    m_instance = nullptr;
}
