/***************************************************************************
    Copyright (C) 2003-2024 Robby Stephenson <robby@periapsis.org>
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   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) version 3 or any later version        *
 *   accepted by the membership of KDE e.V. (or its successor approved     *
 *   by the membership of KDE e.V.), which shall act as a proxy            *
 *   defined in Section 14 of version 3 of the license.                    *
 *                                                                         *
 *   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, see <http://www.gnu.org/licenses/>. *
 *                                                                         *
 ***************************************************************************/

#include "imagefactory.h"
#include "image.h"
#include "imageinfo.h"
#include "imagedirectory.h"
#include "imagejob.h"
#include "../config/tellico_config.h"
#include "../utils/tellico_utils.h"
#include "../utils/gradient.h"
#include "../tellico_debug.h"

#include <KColorUtils>
#include <KZip>
#include <KIO/Global>
#include <KProtocolManager>

#include <QCache>
#include <QFileInfo>
#include <QDir>
#include <QTimer>
#include <QImageReader>

using namespace Tellico;
using Tellico::ImageFactory;

// this image info map is primarily for big images that don't fit
// in the cache, so that don't have to be continually reloaded to get info
QHash<QString, Tellico::Data::ImageInfo> ImageFactory::s_imageInfoMap;
Tellico::StringSet ImageFactory::s_imagesToRelease;

Tellico::ImageFactory* ImageFactory::factory = nullptr;

class ImageFactory::Private {
public:
  Private() = default;

  QHash<QString, Data::Image*> imageDict;
  QCache<QString, Data::Image> imageCache;
  QCache<QString, QPixmap> pixmapCache;
  ImageDirectory dataImageDir; // kept in $HOME/.local/share/tellico/data/
  ImageDirectory localImageDir; // kept local to data file
  TemporaryImageDirectory tempImageDir; // kept in tmp directory
  ImageZipArchive imageZipArchive;
  StringSet nullImages;
  QTimer releaseImagesTimer;
};

inline
bool tryCacheInsert(Tellico::Data::Image* img, QCache<QString, Data::Image>& cache) {
  // if sizeInBytes() is greater than maxCost, then trying and failing to insert it would
  // mean the image gets deleted
  if(img->sizeInBytes() >= cache.maxCost()) {
    myWarning() << "Increasing image cache size to" << 2*img->sizeInBytes();
    cache.setMaxCost(2*img->sizeInBytes());
  }
  return cache.insert(img->id(), img, img->sizeInBytes());
}

ImageFactory::ImageFactory() : QObject(), d(new Private()) {
}

ImageFactory::~ImageFactory() {
  delete d;
}

void ImageFactory::init() {
  if(factory) {
    return;
  }
  factory = new ImageFactory();
  const QUrl dataDir = QUrl::fromLocalFile(Tellico::saveLocation(QStringLiteral("data/")));
  factory->d->dataImageDir.setDirectory(dataDir);
  myLog() << "Setting data image dir:" << factory->d->dataImageDir.dir().toDisplayString(QUrl::PreferLocalFile);

  myLog() << "Setting max image cache cost:" << Config::imageCacheSize();
  factory->d->imageCache.setMaxCost(Config::imageCacheSize());
  myLog() << "Setting max pixmap cache cost:" << Config::imageCacheSize();
  factory->d->pixmapCache.setMaxCost(Config::imageCacheSize());

  factory->d->releaseImagesTimer.setSingleShot(true);
  connect(&factory->d->releaseImagesTimer, &QTimer::timeout, factory, &ImageFactory::releaseImages);

  QImageReader::setAllocationLimit(std::max(QImageReader::allocationLimit(), 2 * 1024)); // 2 GB minimum
}

Tellico::ImageFactory* ImageFactory::self() {
  Q_ASSERT_X(factory, "ImageFactory::self", "ImageFactory is not initialized!");
  return factory;
}

QUrl ImageFactory::tempDir() {
  return factory->d->tempImageDir.dir();
}

QUrl ImageFactory::dataDir() {
  return factory->d->dataImageDir.dir();
}

QUrl ImageFactory::localDir() {
  const auto dir = factory->d->localImageDir.dir();
  return dir.isEmpty() ? dataDir() : dir;
}

QUrl ImageFactory::imageDir() {
  Q_ASSERT_X(factory, "ImageFactory::imageDir()", "ImageFactory is not initialized!");
  switch(cacheDir()) {
    case LocalDir: return localDir();
    case DataDir: return dataDir();
    case ZipArchive: return tempDir();
    case TempDir: return tempDir();
  }
  return tempDir();
}

Tellico::ImageFactory::CacheDir ImageFactory::cacheDir() {
  CacheDir dir = TempDir;
  switch(Config::imageLocation()) {
    case Config::ImagesInLocalDir: dir = LocalDir; break;
    case Config::ImagesInAppDir:   dir = DataDir;  break;
    case Config::ImagesInFile:     dir = TempDir;  break;
  }
  // special case when configured to use local dir but for a new collection when no local dir exists
  if(dir == LocalDir && factory->d->localImageDir.dir().isEmpty()) {
    dir = TempDir;
  }
  return dir;
}

QString ImageFactory::addImage(const QUrl& url_, bool quiet_, const QUrl& refer_, bool link_) {
  Q_ASSERT_X(factory, "ImageFactory::addImage", "ImageFactory is not initialized!");
  return factory->addImageImpl(url_, quiet_, refer_, link_).id();
}

const Tellico::Data::Image& ImageFactory::addImageImpl(const QUrl& url_, bool quiet_, const QUrl& refer_, bool link_) {
  if(url_.isEmpty() || !url_.isValid() || d->nullImages.contains(url_.url())) {
    myDebug() << "Returning null image";
    return Data::Image::null;
  }
  myLog() << "Loading image from url:" << url_.toDisplayString(QUrl::PreferLocalFile | QUrl::NormalizePathSegments);
  ImageJob* job = new ImageJob(url_, QString(), quiet_);
  job->setLinkOnly(link_);
  job->setReferrer(refer_);

  if(!job->exec()) {
    myWarning() << "ImageJob failed:" << job->errorString();
    // ERR_UNKNOWN is used when the returned image is truly null
    // rather than network error or some such
    if(job->error() == KIO::ERR_UNKNOWN) {
      d->nullImages.add(url_.url());
    }
    return Data::Image::null;
  }

  const Data::Image& img = job->image();
  if(img.isNull()) {
    myDebug() << "Null image for" << url_.toDisplayString(QUrl::PreferLocalFile | QUrl::NormalizePathSegments);
    return Data::Image::null;
  }

  // hold the image in memory since it probably isn't written locally to disk yet
  bool putInDict = true;
  if(link_ && url_.isLocalFile()) {
    putInDict = !tryCacheInsert(new Data::Image(img), d->imageCache);
  }
  if(putInDict && !d->imageDict.contains(img.id())) {
    d->imageDict.insert(img.id(), new Data::Image(img));
  }
  s_imageInfoMap.insert(img.id(), Data::ImageInfo(img));
  return img;
}

QString ImageFactory::addImage(const QImage& image_, const QString& format_) {
  Q_ASSERT_X(factory, "ImageFactory::addImage(QImage)", "ImageFactory is not initialized!");
  return factory->addImageImpl(image_, format_).id();
}

QString ImageFactory::addImage(const QPixmap& pix_, const QString& format_) {
  Q_ASSERT_X(factory, "ImageFactory::addImage(QPixmap)", "ImageFactory is not initialized!");
  return factory->addImageImpl(pix_.toImage(), format_).id();
}

const Tellico::Data::Image& ImageFactory::addImageImpl(const QImage& image_, const QString& format_) {
  Data::Image* img = new Data::Image(image_, format_);
  if(hasImageInMemory(img->id())) {
    const Data::Image& img2 = imageById(img->id());
    if(!img2.isNull()) {
      delete img;
      return img2;
    }
  }
  if(img->isNull()) {
    delete img;
    return Data::Image::null;
  }
  myLog() << "Loading image from pixmap:" << img->id();
  d->imageDict.insert(img->id(), img);
  s_imageInfoMap.insert(img->id(), Data::ImageInfo(*img));
  return *img;
}

QString ImageFactory::addImage(const QByteArray& data_, const QString& format_, const QString& id_) {
  Q_ASSERT_X(factory, "ImageFactory::addImage(QByteArray)", "ImageFactory is not initialized!");
  return factory->addImageImpl(data_, format_, id_).id();
}

const Tellico::Data::Image& ImageFactory::addImageImpl(const QByteArray& data_, const QString& format_,
                                                       const QString& id_) {
  Data::Image* img;
  if(!id_.isEmpty()) {
    // do not call imageById(), it causes infinite looping with Document::loadImage()
    img = d->imageCache.object(id_);
    if(img) {
      myLog() << "already exists in cache: " << id_;
      return *img;
    }
    img = d->imageDict.value(id_);
    if(img) {
      myLog() << "already exists in dict: " << id_;
      return *img;
    }
  }
  img = new Data::Image(data_, format_, id_);
  if(img->isNull()) {
    myDebug() << "NULL IMAGE!!!!!";
    delete img;
    return Data::Image::null;
  }

//  myLog() << "format = " << format_ << ", id = "<< img->id();
  myLog() << "Loading image from data:" << img->id();
  d->imageDict.insert(img->id(), img);
  s_imageInfoMap.insert(img->id(), Data::ImageInfo(*img));
  return *img;
}

const Tellico::Data::Image& ImageFactory::addCachedImageImpl(const QString& id_, CacheDir dir_) {
//  myLog() << "Adding cached image:" << id_ << dir_;
  Data::Image* img = nullptr;
  switch(dir_) {
    case DataDir:
      img = d->dataImageDir.imageById(id_);
      break;
    case LocalDir:
      img = d->localImageDir.imageById(id_);
      break;
    case TempDir:
      img = d->tempImageDir.imageById(id_);
      break;
    case ZipArchive:
      img = d->imageZipArchive.imageById(id_);
      break;
  }
  if(!img) {
    myWarning() << "Image not found:" << id_ << "; Cache dir:" << dir_;
    return Data::Image::null;
  }

  s_imageInfoMap.insert(img->id(), Data::ImageInfo(*img));
  if(!tryCacheInsert(img, d->imageCache)) {
    // at this point, img has been deleted!
    myWarning() << "Unable to insert into image cache";
    return Data::Image::null;
  }
  s_imagesToRelease.add(img->id());
  return *img;
}

bool ImageFactory::writeCachedImage(const QString& id_, CacheDir dir_, bool force_ /*=false*/) {
  if(id_.isEmpty()) {
    return false;
  }
//  myLog() << "dir =" << (dir_ == DataDir ? "DataDir" : "TmpDir" ) << "; id =" << id_;
  ImageDirectory* imgDir = nullptr;
  switch(dir_) {
    case DataDir:
      imgDir = &factory->d->dataImageDir;
      break;
    case TempDir:
      imgDir = &factory->d->tempImageDir;
      break;
    case LocalDir:
      // special case when configured to use local dir but for a new collection when no local dir exists
      imgDir = factory->d->localImageDir.dir().isEmpty() ?
                 &factory->d->tempImageDir :
                 &factory->d->localImageDir;
      break;
    case ZipArchive:
      myDebug() << "writeCachedImage() - ZipArchive - should never be called";
      imgDir = &factory->d->tempImageDir;
      break;
  }

  Q_ASSERT(imgDir);
  bool success = writeCachedImage(id_, imgDir, force_);

  if(success) {
    // remove from dict and add to cache
    // it might not be in dict though
    if(factory->d->imageDict.contains(id_)) {
      Data::Image* img = factory->d->imageDict.take(id_);
      Q_ASSERT(img);
      // imageCache.insert will delete the image by itself if the cost exceeds the cache size
      if(tryCacheInsert(img, factory->d->imageCache)) {
        s_imageInfoMap.remove(id_);
        s_imagesToRelease.add(id_);
      }
    }
  }
  return success;
}

bool ImageFactory::writeCachedImage(const QString& id_, ImageDirectory* imgDir_, bool force_ /*=false*/) {
  if(id_.isEmpty() || !imgDir_) {
    return false;
  }
//  myLog() << "ImageFactory::writeCachedImage() - dir =" << imgDir_->dir().path() << "; id =" << id_;
  const bool exists = imgDir_->hasImage(id_);
  // only write if it doesn't exist
  bool success = (!force_ && exists);
  if(!success) {
    const Data::Image& img = imageById(id_);
    if(!img.isNull()) {
      success = imgDir_->writeImage(img);
    }
  }
  return success;
}

const Tellico::Data::Image& ImageFactory::imageById(const QString& id_) {
  Q_ASSERT_X(factory, "ImageFactory::imageById", "ImageFactory is not initialized!");
  if(id_.isEmpty() || !factory || factory->d->nullImages.contains(id_)) {
    return Data::Image::null;
  }
//  myLog() << "Calling imageById:" << id_;

  // can't think of a better place to regularly check for images to release
  // but don't release image that just got asked for
  s_imagesToRelease.remove(id_);
  factory->d->releaseImagesTimer.start(5000);

  // first check the cache, used for images that are in the data file, or are only temporary
  // then the dict, used for images downloaded, but not yet saved anywhere
  Data::Image* img = factory->d->imageCache.object(id_);
  if(img) {
//    myLog() << "...found in cache";
    return *img;
  }

  img = factory->d->imageDict.value(id_);
  if(img) {
//    myLog() << "...found in dict";
    return *img;
  }

  // if the image is link only, we need to load it
  // but can't call imageInfo() since that might recurse into imageById()
  // also, the image info cache might not have it so check if the
  // id is a valid absolute url
  const QUrl imageUrl(id_);
  if((s_imageInfoMap.contains(id_) && s_imageInfoMap[id_].linkOnly) || !imageUrl.isRelative()) {
    if(imageUrl.isValid()) {
//      myLog() << "...adding image by url:" << id_;
      return factory->addImageImpl(imageUrl, true, QUrl(), true);
    }
  }

  // the document does a delayed loading of the images, sometimes
  // so an image could be in the tmp dir and not be in the cache
  // or it could be too big for the cache
  if(factory->d->tempImageDir.hasImage(id_)) {
    const Data::Image& img2 = factory->addCachedImageImpl(id_, TempDir);
    if(!img2.isNull()) {
//      myLog() << "...found in temp dir";
      return img2;
    }
  }

  // try to do a delayed loading of the image
  if(factory->d->imageZipArchive.hasImage(id_)) {
    const Data::Image& img2 = factory->addCachedImageImpl(id_, ZipArchive);
    if(!img2.isNull()) {
//      myLog() << "found in zip archive";
      // go ahead and write image to disk so we don't have to keep it in memory
      // calling pixmap() could be loading all the covers, and we don't want one
      // to get pushed out of the cache yet
      writeCachedImage(id_, TempDir);
      return img2;
    }
  }

  // This section uses the config image setting plus the fallback location
  // to provide confidence that the user's image can be found
  CacheDir configLoc = TempDir;
  CacheDir fallbackLoc = TempDir;
  ImageDirectory* configImgDir = nullptr;
  ImageDirectory* fallbackImgDir = nullptr;
  if(Config::imageLocation() == Config::ImagesInLocalDir) {
    configLoc = LocalDir;
    fallbackLoc = DataDir;
    configImgDir = &factory->d->localImageDir;
    fallbackImgDir = &factory->d->dataImageDir;
  } else if(Config::imageLocation() == Config::ImagesInAppDir) {
    configLoc = DataDir;
    fallbackLoc = LocalDir;
    configImgDir = &factory->d->dataImageDir;
    fallbackImgDir = &factory->d->localImageDir;
  }

  // check the configured location first
  if(configImgDir && configImgDir->hasImage(id_)) {
    const Data::Image& img2 = factory->addCachedImageImpl(id_, configLoc);
    if(!img2.isNull()) {
//      myLog() << "Found image in configured location" << configImgDir->dir().toDisplayString(QUrl::PreferLocalFile) << id_;
      return img2;
    } else {
      myDebug() << "Failed to load" << id_ << "from configured" << configImgDir->dir().toDisplayString(QUrl::PreferLocalFile);
    }
  } else if(fallbackImgDir && fallbackImgDir->hasImage(id_)) {
    const Data::Image& img2 = factory->addCachedImageImpl(id_, fallbackLoc);
    if(!img2.isNull()) {
      myLog() << "Found image in fallback location" << fallbackImgDir->dir().toDisplayString(QUrl::PreferLocalFile) << id_;
      // the img is in the other location
      factory->emitImageMismatch();
      return img2;
    } else {
      myDebug() << "Failed to add" << id_ << "from fallback" << fallbackImgDir->dir().toDisplayString(QUrl::PreferLocalFile);
    }
  }
  // at this point, there's a possibility that the user has changed settings so that the images
  // are currently in a local or data directory, but Config::imageLocation() doesn't match that location
  if(factory->d->dataImageDir.hasImage(id_)) {
    const Data::Image& img2 = factory->addCachedImageImpl(id_, DataDir);
    if(!img2.isNull()) {
      myLog() << "Unexpectedly found image in data dir location:" << id_;
      return img2;
    }
  }
  if(factory->d->localImageDir.hasImage(id_)) {
    const Data::Image& img2 = factory->addCachedImageImpl(id_, LocalDir);
    if(!img2.isNull()) {
      myLog() << "Unexpectedly found image in local dir location:" << id_;
      return img2;
    }
  }
  // now, it appears the image doesn't exist. The only remaining possibility
  // is that the file name has multiple periods and as a result of the fix for bug 348088,
  // the image is lurking in a local image directory without the multiple periods
  if(Config::imageLocation() == Config::ImagesInLocalDir &&
     localDir().isLocalFile() &&
     QDir(localDir().path()).dirName().contains(QLatin1Char('.'))) {
    QString realImageDir = localDir().toLocalFile();
    QDir d(realImageDir);
    // try to cd up and into the other old directory
    QString cdString = QLatin1String("../") + d.dirName().section(QLatin1Char('.'), 0, 0) + QLatin1String("_files/");
    if(d.cd(cdString)) {
      factory->d->localImageDir.setDirectory(QUrl::fromLocalFile(d.path() + QDir::separator()));
      if(factory->d->localImageDir.hasImage(id_)) {
        myLog() << "Reading image from old local directory" << (cdString+id_);
        const Data::Image& img2 = factory->addCachedImageImpl(id_, LocalDir);
        // Be sure to reset the image directory location!!
        factory->d->localImageDir.setDirectory(QUrl::fromLocalFile(realImageDir));
        factory->d->localImageDir.writeImage(img2);
        return img2;
      }
      factory->d->localImageDir.setDirectory(QUrl::fromLocalFile(realImageDir));
    }
  }

  myLog() << "***ImageFactory::imageById() - not found:" << id_;
  return Data::Image::null;
}

bool ImageFactory::hasImageInDir(const QString& id_) {
  Q_ASSERT_X(factory, "ImageFactory::hasImageInDir", "ImageFactory is not initialized!");
  if(id_.isEmpty() || !factory) {
    return false;
  }

  return (Config::imageLocation() == Config::ImagesInLocalDir &&
          factory->d->localImageDir.hasImage(id_)) ||
         (Config::imageLocation() == Config::ImagesInAppDir &&
          factory->d->dataImageDir.hasImage(id_));
}

bool ImageFactory::hasImageInDirOrMemory(const QString& id_) {
  Q_ASSERT_X(factory, "ImageFactory::hasImageInDirOrMemory", "ImageFactory is not initialized!");
  if(id_.isEmpty() || !factory) {
    return false;
  }

  return hasImageInDir(id_) ||
         factory->d->imageCache.contains(id_) ||
         factory->d->imageDict.contains(id_) ||
         factory->d->tempImageDir.hasImage(id_) ||
         factory->d->imageZipArchive.hasImage(id_);
  // Versions prior to 4.1.1 returned true for absolute local file paths
  // Now they're treated identically to remote paths
}

void ImageFactory::requestImageById(const QString& id_) {
  Q_ASSERT_X(factory, "ImageFactory::requestImageById()", "ImageFactory is not initialized!");
  if(hasImageInDirOrMemory(id_)) {
    QTimer::singleShot(0, factory, [id_] () {
      auto img = factory->addCachedImageImpl(id_, cacheDir());
      if(!img.isNull()) {
        Q_EMIT factory->imageAvailable(id_);
      }
    });
    return;
  }
  if(factory->d->nullImages.contains(id_)) {
    // don't emit anything
    return;
  }
  const QUrl u(id_);
  // what does it mean when the Id is an absolute Url and yet the image is not link only?
  // Probably a heritage image id before the bugs were fixed.
  const bool linkOnly = (s_imageInfoMap.contains(id_) && s_imageInfoMap[id_].linkOnly);
  if(linkOnly || !u.isRelative()) {
    if(u.isValid()) {
      factory->requestImageByUrlImpl(u, true /* quiet */, QUrl() /* referrer */, linkOnly);
      if(!linkOnly) {
        myDebug() << "Loading an image url that is not link-only. The image id will get updated.";
        myDebug() << id_;
      }
    }
    return;
  }
  // real fallback, just as ::imageById() checks, look in fall back directories, just in case
  // possible scenario is that the user changed the image location for one data file, but the other file still
  // has images in app or local directory
  int realImageDir = -1;
  if(factory->d->dataImageDir.hasImage(id_)) {
    realImageDir = DataDir;
  } else if(factory->d->localImageDir.hasImage(id_)) {
    realImageDir = LocalDir;
  }

  if(realImageDir > -1) {
    QTimer::singleShot(0, factory, [id_, realImageDir] () {
      auto img = factory->addCachedImageImpl(id_, Tellico::ImageFactory::CacheDir(realImageDir));
      if(!img.isNull()) {
        Q_EMIT factory->imageAvailable(id_);
      }
    });
    return;
  }
}

void ImageFactory::requestImageByUrlImpl(const QUrl& url_, bool quiet_, const QUrl& refer_, bool link_) {
  auto job = new ImageJob(url_, QString() /* id, use calculated one */, quiet_);
  job->setLinkOnly(link_);
  job->setReferrer(refer_);
  connect(job, &ImageJob::result,
          this, &ImageFactory::slotImageJobResult);
}

Tellico::Data::ImageInfo ImageFactory::imageInfo(const QString& id_) {
  if(s_imageInfoMap.contains(id_)) {
    return s_imageInfoMap[id_];
  }

  const Data::Image& img = imageById(id_);
  if(img.isNull()) {
    return Data::ImageInfo();
  }
  return Data::ImageInfo(img);
}

void ImageFactory::cacheImageInfo(const Tellico::Data::ImageInfo& info) {
  s_imageInfoMap.insert(info.id, info);
}

bool ImageFactory::hasImageInfo(const QString& id_) {
  return s_imageInfoMap.contains(id_);
}

bool ImageFactory::validImage(const QString& id_) {
  // don't try s_imageInfoMap[id_] cause it inserts an empty image info
  return s_imageInfoMap.contains(id_) || factory->hasImageInMemory(id_) || !imageById(id_).isNull();
}

QPixmap ImageFactory::pixmap(const QString& id_, int width_, int height_) {
  if(id_.isEmpty()) {
    return QPixmap();
  }

  const QString key = id_ + QLatin1Char('|') + QString::number(width_) + QLatin1Char('|') + QString::number(height_);
  QPixmap* pix = factory->d->pixmapCache.object(key);
  if(pix) {
    return *pix;
  }

  const Data::Image& img = imageById(id_);
  if(img.isNull()) {
    return QPixmap();
  }

  if(width_ > 0 && height_ > 0) {
    pix = new QPixmap(img.convertToPixmap(width_, height_));
  } else {
    pix = new QPixmap(img.convertToPixmap());
  }

  QPixmap pix2(*pix); // retain a copy of pix in case it doesn't go into the cache
  // pixmap size is w x h x d, divided by 8 bits
  const int size = (pix->width()*pix->height()*pix->depth()/8);
  if(!factory->d->pixmapCache.insert(key, pix, size)) {
    // at this point, pix might be deleted
    myWarning() << "can't save in cache: " << id_;
    myWarning() << "### Current pixmap size is " << size;
    myWarning() << "### Max pixmap cache size is " << factory->d->pixmapCache.maxCost();
    return pix2;
  }
  return *pix;
}

void ImageFactory::clean(bool purgeTempDirectory_) {
  // the caches all auto-delete
  s_imagesToRelease.clear();
  qDeleteAll(factory->d->imageDict);
  factory->d->imageDict.clear();
  s_imageInfoMap.clear();
  factory->d->imageCache.clear();
  factory->d->pixmapCache.clear();
  if(purgeTempDirectory_) {
    myLog() << "Purging images from the temporary directory";
    factory->d->tempImageDir.purge();
    // just to make sure all the image locations clean themselves up
    // delete the factory (which deletes the storage objects) and
    // then recreate the factory, in case anything else needs it
    // be sure to save local image directory if it's not a temp dir
    // or the data dir, avoid setLocalDirectory
    const auto localDirName = factory->d->localImageDir.dir();
    delete factory;
    factory = nullptr;
    init(); // now factory is no longer null
    const bool remoteOrLocalExists = !localDirName.isLocalFile() || QDir(localDirName.toLocalFile()).exists();
    if(!localDirName.isEmpty() && remoteOrLocalExists) {
      factory->d->localImageDir.setDirectory(localDirName);
    }
  }
}

void ImageFactory::createStyleImages(int collectionType_, const Tellico::StyleOptions& opt_) {
  myLog() << "Creating style images for type:" << collectionType_;
  const QColor& baseColor = opt_.baseColor.isValid()
                          ? opt_.baseColor
                          : Config::templateBaseColor(collectionType_);
  const QColor& highColor = opt_.highlightedBaseColor.isValid()
                          ? opt_.highlightedBaseColor
                          : Config::templateHighlightedBaseColor(collectionType_);

  const QString bgname(QStringLiteral("gradient_bg.png"));
  const QColor& bgc1 = KColorUtils::mix(baseColor, highColor, 0.3);
  QImage bgImage = Tellico::gradient(QSize(600, 1), bgc1, baseColor,
                                     Tellico::PipeCrossGradient);
  bgImage = bgImage.transformed(QTransform().rotate(90));

  const QString hdrname(QStringLiteral("gradient_header.png"));
  const QColor& bgc2 = KColorUtils::mix(baseColor, highColor, 0.5);
  QImage hdrImage = Tellico::unbalancedGradient(QSize(1, 10), highColor, bgc2,
                                                Tellico::VerticalGradient, 100, -100);

  if(opt_.imgDir.isEmpty()) {
    ImageFactory::removeImage(bgname, true /*delete */);
    factory->addImageImpl(Data::Image::byteArray(bgImage, "PNG"), QStringLiteral("PNG"), bgname);
    ImageFactory::writeCachedImage(bgname, cacheDir(), true /*force*/);

    ImageFactory::removeImage(hdrname, true /*delete */);
    factory->addImageImpl(Data::Image::byteArray(hdrImage, "PNG"), QStringLiteral("PNG"), hdrname);
    ImageFactory::writeCachedImage(hdrname, cacheDir(), true /*force*/);
  } else {
    bgImage.save(opt_.imgDir + bgname, "PNG");
    hdrImage.save(opt_.imgDir + hdrname, "PNG");
  }
}

void ImageFactory::removeImage(const QString& id_, bool deleteImage_) {
//  myLog() << "Removing image from cache:" << id_;
  // be careful using this
  delete factory->d->imageDict.take(id_);
  factory->d->imageCache.remove(id_);

  if(deleteImage_) {
//    myLog() << "Deleting image:" << id_;
    // remove from everywhere
    factory->d->dataImageDir.removeImage(id_);
    factory->d->localImageDir.removeImage(id_);
    factory->d->tempImageDir.removeImage(id_);
  }
}

Tellico::StringSet ImageFactory::imagesNotInCache() {
  StringSet set;
  QHash<QString, Tellico::Data::Image*>::const_iterator end = factory->d->imageDict.constEnd();
  for(QHash<QString, Tellico::Data::Image*>::const_iterator it = factory->d->imageDict.constBegin(); it != end; ++it) {
    if(!factory->d->imageCache.contains(it.key())) {
      set.add(it.key());
    }
  }
  return set;
}

bool ImageFactory::hasImageInMemory(const QString& id_) const {
  return d->imageCache.contains(id_) || d->imageDict.contains(id_);
}

bool ImageFactory::hasNullImage(const QString& id_) const {
  return d->nullImages.contains(id_);
}

// the purpose here is to remove images from the dict if they're is on the disk somewhere,
// either in tempDir() or in dataDir(). The use for this is for calling pixmap() on an
// image too big to stay in the cache. Then it stays in the dict forever.
void ImageFactory::releaseImages() {
  if(s_imagesToRelease.isEmpty()) {
    return;
  }

  myLog() << "Releasing" << s_imagesToRelease.count() << "images from internal memory";
  for(const QString& id : std::as_const(s_imagesToRelease)) {
    if(!d->imageDict.contains(id)) {
      continue;
    }
    if(d->dataImageDir.hasImage(id) ||
       d->localImageDir.hasImage(id) ||
       d->tempImageDir.hasImage(id)) {
      delete d->imageDict.take(id);
    }
  }
  s_imagesToRelease.clear();
}

void ImageFactory::emitImageMismatch() {
  Q_EMIT imageLocationMismatch();
}

QUrl ImageFactory::localDirectory(const QUrl& url_) {
  if(url_.isEmpty()) {
    return QUrl();
  }
  if(!url_.isLocalFile() && !KProtocolManager::supportsWriting(url_)) {
    myWarning() << "Unable to save images local to" << url_.toDisplayString();
    return QUrl();
  }
  QUrl dirUrl = url_.adjusted(QUrl::RemoveFilename);
  // could have already been set once
  if(!dirUrl.path().endsWith(QLatin1String("_files/"))) {
    QFileInfo fi(url_.fileName());
    dirUrl.setPath(dirUrl.path() + fi.completeBaseName() + QLatin1String("_files/"));
  }
  return dirUrl;
}

void ImageFactory::setLocalDirectory(const QUrl& url_) {
  Q_ASSERT_X(factory, "ImageFactory::setLocalDirectory", "ImageFactory is not initialized!");
  const auto localDirName = localDirectory(url_);
  if(localDirName.isEmpty()) {
    myLog() << "Clearing the local directory name";
    factory->d->localImageDir.reset();
  } else {
    myLog() << "Setting local image dir:" << localDirName.toDisplayString(QUrl::PreferLocalFile);
    factory->d->localImageDir.setDirectory(localDirName);
  }
}

void ImageFactory::setZipArchive(std::unique_ptr<KZip> zip_) {
  Q_ASSERT_X(factory, "ImageFactory::setZipArchive()", "ImageFactory is not initialized!");
  if(!zip_) {
    return;
  }
  factory->d->imageZipArchive.setZip(std::move(zip_));
}

void ImageFactory::slotImageJobResult(KJob* job_) {
  ImageJob* imageJob = qobject_cast<ImageJob*>(job_);
  Q_ASSERT(imageJob);
  if(!imageJob) {
    myWarning() << "No image job";
    return;
  }
  const Data::Image& img = imageJob->image();
  if(img.isNull()) {
    myDebug() << "Null image for" << imageJob->url().url(QUrl::PreferLocalFile);
    d->nullImages.add(imageJob->url().url());
    // don't emit anything
    return;
  }

  // hold the image in memory since it probably isn't written locally to disk yet
  // unless it's a local link only
  bool putInDict = true;
  if(imageJob->linkOnly() && imageJob->url().isLocalFile()) {
    putInDict = !tryCacheInsert(new Data::Image(img), d->imageCache);
  }
  if(putInDict && !d->imageDict.contains(img.id())) {
    d->imageDict.insert(img.id(), new Data::Image(img));
  }
  s_imageInfoMap.insert(img.id(), Data::ImageInfo(img));
  s_imagesToRelease.add(img.id());
  Q_EMIT factory->imageAvailable(img.id());
}
