/***************************************************************************
    copyright            : (C) 2003-2006 by Robby Stephenson
    email                : robby@periapsis.org
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of version 2 of the GNU General Public License as  *
 *   published by the Free Software Foundation;                            *
 *                                                                         *
 ***************************************************************************/

#include "imagefactory.h"
#include "image.h"
#include "document.h"
#include "filehandler.h"
#include "tellico_utils.h"
#include "tellico_kernel.h"
#include "core/tellico_config.h"
#include "tellico_debug.h"

#include <ktempdir.h>
#include <tdeapplication.h>
#include <kimageeffect.h>

#include <tqfile.h>
#include <tqdir.h>

#define RELEASE_IMAGES

using Tellico::ImageFactory;

bool ImageFactory::s_needInit = true;
const Tellico::Data::Image ImageFactory::s_null;

TQDict<Tellico::Data::Image> ImageFactory::s_imageDict;
// since most images get turned into pixmaps quickly, use 10 megs
// for images and 10 megs for pixmaps
TQCache<Tellico::Data::Image> ImageFactory::s_imageCache(10 * 1024 * 1024);
TQCache<TQPixmap> ImageFactory::s_pixmapCache(10 * 1024 * 1024);
// this image info map is just for big images that don't fit
// in the cache, so that don't have to be continually reloaded to get info
TQMap<TQString, Tellico::Data::ImageInfo> ImageFactory::s_imageInfoMap;
Tellico::StringSet ImageFactory::s_imagesInTmpDir;
Tellico::StringSet ImageFactory::s_imagesToRelease;
KTempDir* ImageFactory::s_tmpDir = 0;
TQString ImageFactory::s_localDir;

void ImageFactory::init() {
  if(!s_needInit) {
    return;
  }
  s_imageDict.setAutoDelete(true);
  s_imageCache.setAutoDelete(true);
  s_imageCache.setMaxCost(Config::imageCacheSize());
  s_pixmapCache.setAutoDelete(true);
  s_needInit = false;
}

TQString ImageFactory::tempDir() {
  if(!s_tmpDir) {
    s_tmpDir = new KTempDir();
    s_tmpDir->setAutoDelete(true);
  }
  return s_tmpDir->name();
}

TQString ImageFactory::dataDir() {
  static const TQString dataDir = Tellico::saveLocation(TQString::fromLatin1("data/"));
  return dataDir;
}

TQString ImageFactory::localDir() {
  if(s_localDir.isEmpty()) {
    return dataDir();
  }
  return s_localDir;
}

TQString ImageFactory::addImage(const KURL& url_, bool quiet_, const KURL& refer_, bool link_) {
  return addImageImpl(url_, quiet_, refer_, link_).id();
}

const Tellico::Data::Image& ImageFactory::addImageImpl(const KURL& url_, bool quiet_, const KURL& refer_, bool link_) {
  if(url_.isEmpty() || !url_.isValid()) {
    return s_null;
  }
//  myLog() << "ImageFactory::addImageImpl(KURL) - " << url_.prettyURL() << endl;
  Data::Image* img = refer_.isEmpty()
                     ? FileHandler::readImageFile(url_, quiet_)
                     : FileHandler::readImageFile(url_, quiet_, refer_);
  if(!img) {
    myLog() << "ImageFactory::addImageImpl() - image not found: " << url_.prettyURL() << endl;
    return s_null;
  }
  if(img->isNull()) {
    delete img;
    return s_null;
  }

  if(link_) {
    img->setLinkOnly(true);
    img->setID(url_.url());
  }

  if(hasImage(img->id())) {
//    myDebug() << "### ImageFactory::addImageImpl() - hasImage() is true!" << endl;
    const Data::Image& img2 = imageById(img->id());
    if(!img2.isNull()) {
      delete img;
      return img2;
    }
  }

  if(!link_) {
    s_imageDict.insert(img->id(), img);
  }
  s_imageInfoMap.insert(img->id(), Data::ImageInfo(*img));
  return *img;
}

TQString ImageFactory::addImage(const TQImage& image_, const TQString& format_) {
  return addImageImpl(image_, format_).id();
}

TQString ImageFactory::addImage(const TQPixmap& pix_, const TQString& format_) {
  return addImageImpl(pix_.convertToImage(), format_).id();
}

const Tellico::Data::Image& ImageFactory::addImageImpl(const TQImage& image_, const TQString& format_) {
  Data::Image* img = new Data::Image(image_, format_);
  if(hasImage(img->id())) {
    const Data::Image& img2 = imageById(img->id());
    if(!img2.isNull()) {
      delete img;
      return img2;
    }
  }
  if(img->isNull()) {
    delete img;
    return s_null;
  }
  s_imageDict.insert(img->id(), img);
  s_imageInfoMap.insert(img->id(), Data::ImageInfo(*img));
  return *img;
}

TQString ImageFactory::addImage(const TQByteArray& data_, const TQString& format_, const TQString& id_) {
  return addImageImpl(data_, format_, id_).id();
}

const Tellico::Data::Image& ImageFactory::addImageImpl(const TQByteArray& data_, const TQString& format_,
                                                       const TQString& id_) {
  if(id_.isEmpty()) {
    return s_null;
  }

  // do not call imageById(), it causes infinite looping with Document::loadImage()
  Data::Image* img = s_imageCache.find(id_);
  if(img) {
    myLog() << "ImageFactory::addImageImpl(TQByteArray) - already exists in cache: " << id_ << endl;
    return *img;
  }

  img = s_imageDict.find(id_);
  if(img) {
    myLog() << "ImageFactory::addImageImpl(TQByteArray) - already exists in dict: " << id_ << endl;
    return *img;
  }

  img = new Data::Image(data_, format_, id_);
  if(img->isNull()) {
    myDebug() << "ImageFactory::addImageImpl(TQByteArray) - NULL IMAGE!!!!!" << endl;
    delete img;
    return s_null;
  }

//  myLog() << "ImageFactory::addImageImpl(TQByteArray) - " << data_.size()
//          << " bytes, format = " << format_
//          << ", id = "<< img->id() << endl;

  s_imageDict.insert(img->id(), img);
  s_imageInfoMap.insert(img->id(), Data::ImageInfo(*img));
  return *img;
}

const Tellico::Data::Image& ImageFactory::addCachedImageImpl(const TQString& id_, CacheDir dir_) {
//  myLog() << "ImageFactory::addCachedImageImpl() - dir = " << (dir_ == DataDir ? "DataDir" : "TmpDir" )
//                                                           << "; id = " << id_ << endl;
  KURL u;
  if(dir_ == DataDir) {
    u.setPath(dataDir() + id_);
  } else if(dir_ == LocalDir) {
    u.setPath(localDir() + id_);
  } else{ // Temp
    u.setPath(tempDir() + id_);
  }

  TQString newID = addImage(u, true);
  if(newID.isEmpty()) {
    myLog() << "ImageFactory::addCachedImageImpl() - null image loaded" << endl;
    return s_null;
  }

  // the id probably got changed, so reset it
  // addImage() already inserted it in the dict
  Data::Image* img = s_imageDict.take(newID);
  if(!img) {
    kdWarning() << "ImageFactory::addCachedImageImpl() - no image in dict - very bad!" << endl;
    return s_null;
  }
  if(img->isNull()) {
    kdWarning() << "ImageFactory::addCachedImageImpl() - null image in dict, should never happen!" << endl;
    delete img;
    return s_null;
  }
  img->setID(id_);
  s_imageInfoMap.remove(newID);
  s_imageInfoMap.insert(img->id(), Data::ImageInfo(*img));

  if(s_imageCache.insert(img->id(), img, img->numBytes())) {
//    myLog() << "ImageFactory::addCachedImageImpl() - removing from dict: " << img->id() << endl;
  } else {
    // can't hold it in the cache
    kdWarning() << "Tellico's image cache is unable to hold the image, it might be too big!" << endl;
    kdWarning() << "Image name is " << img->id() << endl;
    kdWarning() << "Image size is " << img->numBytes() << endl;
    kdWarning() << "Max cache size is " << s_imageCache.maxCost() << endl;

    // add it back to the dict, but add the image to the list of
    // images to release later. Necessary to avoid a memory leak since new Image()
    // was called, we need to keep the pointer
    s_imageDict.insert(img->id(), img);
    s_imagesToRelease.add(img->id());
  }
  return *img;
}

bool ImageFactory::writeImage(const TQString& id_, const KURL& targetDir_, bool force_) {
//  myLog() << "ImageFactory::writeImage() - target = " << targetDir_.url() << id_ << endl;
  if(targetDir_.isEmpty()) {
    myDebug() << "ImageFactory::writeImage() - empty target dir!" << endl;
    return false;
  }

  const Data::Image& img = imageById(id_);
  if(img.isNull()) {
//    myDebug() << "ImageFactory::writeImage() - null image: " << id_ << endl;
    return false;
  }

  if(img.linkOnly()) {
//    myLog() << "ImageFactory::writeImage() - " << id_ << ": link only, not writing!" << endl;
    return true;
  }

  KURL target = targetDir_;
  target.addPath(id_);

  return FileHandler::writeDataURL(target, img.byteArray(), force_);
}

bool ImageFactory::writeCachedImage(const TQString& id_, CacheDir dir_, bool force_ /*=false*/) {
  if(id_.isEmpty()) {
    return false;
  }
//  myLog() << "ImageFactory::writeCachedImage() - dir = " << (dir_ == DataDir ? "DataDir" : "TmpDir" )
//                                                         << "; id = " << id_ << endl;

  TQString path = ( dir_ == DataDir ? dataDir() : dir_ == TempDir ? tempDir() : localDir() );

  // images in the temp directory are erased every session, so we can track
  // whether they've already been written with a simple string set.
  // images in the data directory are persistent, so we have to check the
  // actual file existence
  bool exists = ( dir_ == TempDir ? s_imagesInTmpDir.has(id_) : TQFile::exists(path + id_));

  if(!force_ && exists) {
//    myDebug() << "...writeCachedImage() - exists = true: " << id_ << endl;
  } else if(!force_ && !exists && dir_ == LocalDir) {
    TQDir dir(localDir());
    if(!dir.exists()) {
      myDebug() << "ImageFactory::writeCachedImage() - creating " << s_localDir << endl;
      dir.mkdir(localDir());
    }
  } else {
//    myLog() << "ImageFactory::writeCachedImage() - dir = " << (dir_ == DataDir ? "DataDir" : "TmpDir" )
//                                                           << "; id = " << id_ << endl;
  }
  // only write if it doesn't exist
  bool success = (!force_ && exists) || writeImage(id_, path, true /* force */);

  if(success) {
    if(dir_ == TempDir) {
      s_imagesInTmpDir.add(id_);
    }

    // remove from dict and add to cache
    // it might not be in dict though
    Data::Image* img = s_imageDict.take(id_);
    if(img && s_imageCache.insert(img->id(), img, img->numBytes())) {
      s_imageInfoMap.remove(id_);
    } else if(img) {
//      myLog() << "ImageFactory::writeCachedImage() - failed writing image to cache: " << id_ << endl;
//      myLog() << "ImageFactory::writeCachedImage() - removed from dict, deleting: " << img->id() << endl;
//      myLog() << "ImageFactory::writeCachedImage() - current dict size: " << s_imageDict.count() << endl;
      // can't insert it in the cache, so put it back in the dict
      // No, it's written to disk now, so we're safe
//      s_imageDict.insert(img->id(), img);
      delete img;
    }
  }
  return success;
}

const Tellico::Data::Image& ImageFactory::imageById(const TQString& id_) {
  if(id_.isEmpty()) {
//    myDebug() << "ImageFactory::imageById() - empty id" << endl;
    return s_null;
  }
//  myLog() << "ImageFactory::imageById() - " << id_ << endl;

  // 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_);
  releaseImages();

 // 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 = s_imageCache.find(id_);
  if(img) {
//    myLog() << "...imageById() - found in cache" << endl;
    return *img;
  }

  img = s_imageDict.find(id_);
  if(img) {
//    myLog() << "...imageById() - found in dict" << endl;
    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
  // yeah, it's probably slow
  if((s_imageInfoMap.contains(id_) && s_imageInfoMap[id_].linkOnly) || !KURL::isRelativeURL(id_)) {
    KURL u = id_;
    if(u.isValid()) {
      return addImageImpl(u, true, KURL(), 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(s_imagesInTmpDir.has(id_)) {
    const Data::Image& img2 = addCachedImageImpl(id_, TempDir);
    if(!img2.isNull()) {
//      myLog() << "...imageById() - found in tmp dir" << endl;
      return img2;
    } else {
      myLog() << "ImageFactory::imageById() - img in tmpDir list but not actually there: " << id_ << endl;
      s_imagesInTmpDir.remove(id_);
    }
  }

  // try to do a delayed loading of the image
  if(Data::Document::self()->loadImage(id_)) {
    // loadImage() could insert in either the cache or the dict!
    img = s_imageCache.find(id_);
    if(!img) {
      img = s_imageDict.find(id_);
    }
    if(img) {
//      myLog() << "...imageById() - found in doc" << endl;
      // 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
      if(!s_imagesInTmpDir.has(id_)) {
        writeCachedImage(id_, TempDir);
      }
      return *img;
    }
  }

  if(Config::imageLocation() == Config::ImagesInLocalDir) {
    bool exists = TQFile::exists(localDir() + id_);
    if(exists) {
      const Data::Image& img2 = addCachedImageImpl(id_, LocalDir);
      if(img2.isNull()) {
        myDebug() << "ImageFactory::imageById() - tried to add from LocalDir, but failed: " << id_ << endl;
      } else {
//        myLog() << "...imageById() - found in local dir" << endl;
        return img2;
      }
    }
    // not an else statement, it might be null
    if(TQFile::exists(dataDir() + id_)) {
      // the img is in the other location
      // consider the document to be modified since it needs the image saved
      Data::Document::self()->slotSetModified(true);
      const Data::Image& img2 = addCachedImageImpl(id_, DataDir);
      if(img2.isNull()) {
        myDebug() << "ImageFactory::imageById() - tried to add from DataDir, but failed: " << id_ << endl;
      } else {
//        myLog() << "...imageById() - found in data dir" << endl;
        return img2;
      }
    }
  } else if(Config::imageLocation() == Config::ImagesInAppDir) {
    bool exists = TQFile::exists(dataDir() + id_);
    if(exists) {
      const Data::Image& img2 = addCachedImageImpl(id_, DataDir);
      if(img2.isNull()) {
        myDebug() << "ImageFactory::imageById() - tried to add from DataDir, but failed: " << id_ << endl;
      } else {
//        myLog() << "...imageById() - found in data dir" << endl;
        return img2;
      }
    }
    // not an else statement, it might be null
    if(TQFile::exists(localDir() + id_)) {
      // the img is in the other location
      // consider the document to be modified since it needs the image saved
      Data::Document::self()->slotSetModified(true);
      const Data::Image& img2 = addCachedImageImpl(id_, LocalDir);
      if(img2.isNull()) {
        myDebug() << "ImageFactory::imageById() - tried to add from LocalDir, but failed: " << id_ << endl;
      } else {
//        myLog() << "...imageById() - found in local dir" << endl;
        return img2;
      }
    }
  }
  myDebug() << "***ImageFactory::imageById() - not found: " << id_ << endl;
  return s_null;
}

Tellico::Data::ImageInfo ImageFactory::imageInfo(const TQString& 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 Data::ImageInfo& info) {
  s_imageInfoMap.insert(info.id, info);
}

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

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

  const TQString key = id_ + '|' + TQString::number(width_) + '|' + TQString::number(height_);
  TQPixmap* pix = s_pixmapCache.find(key);
  if(pix) {
    return *pix;
  }

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

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

  // pixmap size is w x h x d, divided by 8 bits
  if(!s_pixmapCache.insert(key, pix, pix->width()*pix->height()*pix->depth()/8)) {
    kdWarning() << "ImageFactory::pixmap() - can't save in cache: " << id_ << endl;
    kdWarning() << "### Current pixmap size is " << (pix->width()*pix->height()*pix->depth()/8) << endl;
    kdWarning() << "### Max pixmap cache size is " << s_pixmapCache.maxCost() << endl;
    TQPixmap pix2(*pix);
    delete pix;
    return pix2;
  }
  return *pix;
}

void ImageFactory::clean(bool deleteTempDirectory_) {
  // the dict and caches all auto-delete
  s_imagesToRelease.clear();
  s_imageDict.clear();
  s_imageInfoMap.clear();
  s_imageCache.clear();
  s_pixmapCache.clear();
  if(deleteTempDirectory_) {
    s_imagesInTmpDir.clear();
    delete s_tmpDir;
    s_tmpDir = 0;
  }
}

void ImageFactory::createStyleImages(const StyleOptions& opt_) {
  const int collType = Kernel::self()->collectionType();

  const TQColor& baseColor = opt_.baseColor.isValid()
                          ? opt_.baseColor
                          : Config::templateBaseColor(collType);
  const TQColor& highColor = opt_.highlightedBaseColor.isValid()
                          ? opt_.highlightedBaseColor
                          : Config::templateHighlightedBaseColor(collType);

  const TQColor& bgc1 = Tellico::blendColors(baseColor, highColor, 30);
  const TQColor& bgc2 = Tellico::blendColors(baseColor, highColor, 50);

  const TQString bgname = TQString::fromLatin1("gradient_bg.png");
  TQImage bgImage = KImageEffect::gradient(TQSize(400, 1), bgc1, baseColor,
                                          KImageEffect::PipeCrossGradient);
  bgImage = KImageEffect::rotate(bgImage, KImageEffect::Rotate90);

  const TQString hdrname = TQString::fromLatin1("gradient_header.png");
  TQImage hdrImage = KImageEffect::unbalancedGradient(TQSize(1, 10), highColor, bgc2,
                                                     KImageEffect::VerticalGradient, 100, -100);

  if(opt_.imgDir.isEmpty()) {
    // write the style images both to the tmp dir and the data dir
    // doesn't really hurt and lets the user switch back and forth
    ImageFactory::removeImage(bgname, true /*delete */);
    ImageFactory::addImageImpl(Data::Image::byteArray(bgImage, "PNG"), TQString::fromLatin1("PNG"), bgname);
    ImageFactory::writeCachedImage(bgname, DataDir, true /*force*/);
    ImageFactory::writeCachedImage(bgname, TempDir, true /*force*/);

    ImageFactory::removeImage(hdrname, true /*delete */);
    ImageFactory::addImageImpl(Data::Image::byteArray(hdrImage, "PNG"), TQString::fromLatin1("PNG"), hdrname);
    ImageFactory::writeCachedImage(hdrname, DataDir, true /*force*/);
    ImageFactory::writeCachedImage(hdrname, TempDir, true /*force*/);
  } else {
    bgImage.save(opt_.imgDir + bgname, "PNG");
    hdrImage.save(opt_.imgDir + hdrname, "PNG");
  }
}

void ImageFactory::removeImage(const TQString& id_, bool deleteImage_) {
  //be careful using this
  s_imageDict.remove(id_);
  s_imageCache.remove(id_);
  s_imagesInTmpDir.remove(id_);

  if(deleteImage_) {
    // remove from both data dir and temp dir
    TQFile::remove(dataDir() + id_);
    TQFile::remove(tempDir() + id_);
  }
}

Tellico::StringSet ImageFactory::imagesNotInCache() {
  StringSet set;
  for(TQDictIterator<Tellico::Data::Image> it(s_imageDict); it.current(); ++it) {
    if(s_imageCache.find(it.currentKey()) == 0) {
      set.add(it.currentKey());
    }
  }
  return set;
}

bool ImageFactory::hasImage(const TQString& id_) {
  return s_imageCache.find(id_, false) || s_imageDict.find(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() {
#ifdef RELEASE_IMAGES
  if(s_imagesToRelease.isEmpty()) {
    return;
  }

  const TQStringList images = s_imagesToRelease.toList();
  for(TQStringList::ConstIterator it = images.begin(); it != images.end(); ++it) {
    s_imagesToRelease.remove(*it);
    if(!s_imageDict.find(*it)) {
      continue;
    }
//    myLog() << "ImageFactory::releaseImage() - id = " << *it << endl;
    if(TQFile::exists(dataDir() + *it)) {
//      myDebug() << "...exists in dataDir() - removing from dict" << endl;
      s_imageDict.remove(*it);
    } else if(TQFile::exists(tempDir() + *it)) {
//      myDebug() << "...exists in tempDir() - removing from dict" << endl;
      s_imageDict.remove(*it);
    }
  }
#endif
}

void ImageFactory::setLocalDirectory(const KURL& url_) {
  if(url_.isEmpty()) {
    return;
  }
  if(!url_.isLocalFile()) {
    myWarning() << "ImageFactory::setLocalDirectory() - Tellico can only save images to local disk" << endl;
    myWarning() << "unable to save to " << url_ << endl;
  } else {
    s_localDir = url_.directory(false);
    // could have already been set once
    if(!url_.fileName().contains(TQString::fromLatin1("_files"))) {
      s_localDir += url_.fileName().section('.', 0, 0) + TQString::fromLatin1("_files/");
    }
    myLog() << "ImageFactory::setLocalDirectory() - local dir = " << s_localDir << endl;
  }
}

#undef RELEASE_IMAGES
