/FileExtraction/Java_unseen/Kegbot_kegbot-android/kegtab/src/main/java/org/kegbot/app/util/ImageDownloader.java
/*
 * Copyright 2003-2020 The Kegbot Project contributors <info@kegbot.org>
 *
 * This file is part of the Kegtab package from the Kegbot project. For
 * more information on Kegtab or Kegbot, see <http://kegbot.org/>.
 *
 * Kegtab 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, version 2.
 *
 * Kegtab 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 Kegtab. If not, see <http://www.gnu.org/licenses/>.
 */

package org.kegbot.app.util;

import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.ImageView;

import com.google.common.collect.Sets;

import org.apache.commons.codec.binary.Hex;
import org.kegbot.app.R;

import java.io.File;
import java.io.FileOutputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.SoftReference;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * This helper class download images from the Internet and binds those with the provided ImageView.
 * <p/>
 * <p/>
 * A local cache of downloaded images is maintained internally to improve performance.
 */
public class ImageDownloader {
  private static final String TAG = ImageDownloader.class.getSimpleName();

  private static final int HANDLER_KEY_DOWNLOAD_COMPLETE = 1;

  private static final boolean DEBUG = false;

  /**
   * All ImageViews and the URL they have requested.
   */
  private final WeakHashMap<ImageView, String> mDownloadRequests =
      new WeakHashMap<ImageView, String>();

  private static class DownloadResult {
    String url;
    Bitmap bitmap;

    DownloadResult(String url, Bitmap bitmap) {
      this.url = url;
      this.bitmap = bitmap;
    }
  }

  private final ExecutorService mExecutor = Executors.newFixedThreadPool(5);

  private final Context mContext;

  private URL mBaseUrl;

  private File mCacheDir;

  @SuppressLint("HandlerLeak")
  private final Handler mHandler = new Handler(Looper.getMainLooper()) {

    @Override
    public void handleMessage(Message msg) {
      if (msg.what == HANDLER_KEY_DOWNLOAD_COMPLETE) {
        final DownloadResult result = (DownloadResult) msg.obj;
        handleDownloadComplete(result);
      }
      super.handleMessage(msg);
    }
  };

  public ImageDownloader(final Context context, final String baseUrl) {
    mContext = context;
    mCacheDir = context.getCacheDir();
    setBaseUrl(baseUrl);
  }

  public void setBaseUrl(String url) {
    if (url != null) {
      try {
        mBaseUrl = new URL(url);
      } catch (MalformedURLException e) {
        Log.w(TAG, "Bad base url: " + url);
        mBaseUrl = null;
      }
    }
  }

  /**
   * Download the specified image from the Internet and binds it to the provided ImageView. The
   * binding is immediate if the image is found in the cache and will be done asynchronously
   * otherwise. A null bitmap will be associated to the ImageView if an error occurs.
   *
   * @param url       The URL of the image to download.
   * @param imageView The ImageView to bind the downloaded image to.
   */
  public void download(String url, final ImageView imageView) {
    if (mBaseUrl != null) {
      URL fullUrl;
      try {
        fullUrl = new URL(mBaseUrl, url);
      } catch (MalformedURLException e) {
        Log.w(TAG, "Malformed URL: " + url);
        return;
      }
      if (DEBUG) Log.d(TAG, "original url=" + url);
      url = fullUrl.toString();
    }

    if (DEBUG) Log.d(TAG, "download url=" + url + " imageView=" + imageView);
    imageView.setTag(url);
    // resetPurgeTimer();
    final Bitmap bitmap = getBitmapFromCache(url.toString());

    if (bitmap != null) {
      if (DEBUG) Log.d(TAG, "download: cache hit");
      // Bitmap in cache: no download necessary.
      applyBitmapToImageView(bitmap, imageView);
    } else {
      if (DEBUG) Log.d(TAG, "download: cache miss");

      // TODO(mikey): this should be done only in an adapter when convertView !=
      // null
      // imageView.setBackgroundDrawable(null);
      // imageView.setImageBitmap(null);

      // No bitmap in cache: enqueue the download.
      synchronized (mDownloadRequests) {
        if (DEBUG) Log.d(TAG, "download: adding to request queue");
        if (!mDownloadRequests.containsValue(url)) {
          enqueueDownload(url);
        }
        mDownloadRequests.put(imageView, url);
      }
    }
  }

  private void enqueueDownload(final String url) {
    if (DEBUG) Log.d(TAG, "Enqueuing download: url=" + url);
    mExecutor.submit(new Runnable() {
      @Override
      public void run() {
        if (DEBUG) Log.d(TAG, "Download running for url=" + url);

        Bitmap bitmap = getBitmapFromFileCache(url);
        if (bitmap != null) {
          if (DEBUG) Log.d(TAG, "Found bitmap in file cache.");
        } else {
          if (DEBUG) Log.d(TAG, "Download running for url=" + url);
          bitmap = Downloader.downloadBitmap(url);
          Log.d(TAG, "Downloaded: " + url);
          addBitmapToFileCache(url, bitmap);
        }

        addBitmapToCache(url, bitmap);
        postDownloadCompletedToHandler(url, bitmap);
      }
    });
  }

  private void postDownloadCompletedToHandler(String url, Bitmap bitmap) {
    final DownloadResult result = new DownloadResult(url, bitmap);
    final Message msg = mHandler.obtainMessage(HANDLER_KEY_DOWNLOAD_COMPLETE, result);
    mHandler.sendMessage(msg);
  }

  private void applyBitmapToImageView(Bitmap bitmap, ImageView imageView) {
    if (DEBUG) Log.d(TAG, "Assigning bitmap=" + bitmap + " imageView=" + imageView);
    Utils.setBackground(imageView, null);
    imageView.setImageBitmap(bitmap);
    imageView.setAlpha(1.0f);
  }

  private void handleDownloadComplete(DownloadResult downloadResult) {
    final String url = downloadResult.url;
    final Bitmap bitmap = downloadResult.bitmap;
    if (DEBUG) Log.d(TAG, "handleDownloadComplete: url=" + url + " bitmap=" + bitmap);

    final Set<ImageView> toRemove = Sets.newLinkedHashSet();
    synchronized (mDownloadRequests) {
      for (final Map.Entry<ImageView, String> entry : mDownloadRequests.entrySet()) {
        if (url.equals(entry.getValue())) {
          final ImageView imageView = entry.getKey();
          toRemove.add(imageView);

          final String imageViewTag = (String) imageView.getTag();
          if (url.equals(imageViewTag)) {
            applyBitmapToImageView(bitmap, imageView);
            Animation myFadeInAnimation = AnimationUtils.loadAnimation(mContext,
                R.anim.image_fade_in);
            imageView.startAnimation(myFadeInAnimation);
          }
        }
      }

      for (final ImageView view : toRemove) {
        mDownloadRequests.remove(view);
      }
    }
  }

  public void cancelDownloadForView(ImageView view) {
    view.setTag(null);
    synchronized (mDownloadRequests) {
      mDownloadRequests.remove(view);
    }
  }

  /*
   * An InputStream that skips the exact number of bytes provided, unless it
   * reaches EOF.
   */
  static class FlushedInputStream extends FilterInputStream {
    public FlushedInputStream(InputStream inputStream) {
      super(inputStream);
    }

    @Override
    public long skip(long n) throws IOException {
      long totalBytesSkipped = 0L;
      while (totalBytesSkipped < n) {
        long bytesSkipped = in.skip(n - totalBytesSkipped);
        if (bytesSkipped == 0L) {
          int b = read();
          if (b < 0) {
            break; // we reached EOF
          } else {
            bytesSkipped = 1; // we read one byte
          }
        }
        totalBytesSkipped += bytesSkipped;
      }
      return totalBytesSkipped;
    }
  }

  /*
   * Cache-related fields and methods.
   *
   * We use a hard and a soft cache. A soft reference cache is too aggressively
   * cleared by the Garbage Collector.
   */

  private static final int HARD_CACHE_CAPACITY = 10;

  // Hard cache, with a fixed maximum capacity and a life duration
  private final HashMap<String, Bitmap> sHardBitmapCache = new LinkedHashMap<String, Bitmap>(
      HARD_CACHE_CAPACITY / 2, 0.75f, true) {
    @Override
    protected boolean removeEldestEntry(LinkedHashMap.Entry<String, Bitmap> eldest) {
      if (size() > HARD_CACHE_CAPACITY) {
        // Entries push-out of hard reference cache are transferred to soft
        // reference cache
        sSoftBitmapCache.put(eldest.getKey(), new SoftReference<Bitmap>(eldest.getValue()));
        return true;
      } else {
        return false;
      }
    }
  };

  // Soft cache for bitmaps kicked out of hard cache
  private final static ConcurrentHashMap<String, SoftReference<Bitmap>> sSoftBitmapCache =
      new ConcurrentHashMap<String, SoftReference<Bitmap>>(HARD_CACHE_CAPACITY / 2);

  /**
   * Adds this bitmap to the cache.
   *
   * @param bitmap The newly downloaded bitmap.
   */
  private void addBitmapToCache(String url, Bitmap bitmap) {
    if (bitmap != null) {
      synchronized (sHardBitmapCache) {
        sHardBitmapCache.put(url, bitmap);
      }
    }
  }

  private static String getFingerprint(String uri) {
    MessageDigest md;
    try {
      md = MessageDigest.getInstance("SHA-256");
    } catch (NoSuchAlgorithmException e) {
      throw new IllegalStateException(e);
    }
    md.update(uri.getBytes());
    byte[] digest = md.digest();
    return Hex.encodeHexString(digest);
  }

  private File getCacheFilename(String url) {
    return new File(mCacheDir, "ImageDownloader-" + getFingerprint(url));
  }

  private void addBitmapToFileCache(String url, Bitmap bitmap) {
    if (bitmap == null) {
      return;
    }
    final File cacheFile = getCacheFilename(url);
    if (cacheFile.exists()) {
      Log.d(TAG, "Updating cached file url=" + url + " filename=" + cacheFile);
    } else {
      Log.d(TAG, "Creating cached file url=" + url + " filename=" + cacheFile);
    }

    try {
      cacheFile.createNewFile();
      FileOutputStream fos = new FileOutputStream(cacheFile);
      bitmap.compress(Bitmap.CompressFormat.PNG, 85, fos);
      fos.flush();
      fos.close();
    } catch (IOException e) {
      Log.w(TAG, "Error adding cache file.", e);
      cacheFile.delete();
    }

  }

  private Bitmap getBitmapFromFileCache(String url) {
    final File cacheFile = getCacheFilename(url);
    if (cacheFile.exists()) {
      if (DEBUG) Log.d(TAG, "getFromFileCache hit: url=" + url + " filename=" + cacheFile);
    } else {
      if (DEBUG) Log.d(TAG, "getFromFileCache MISS: url=" + url + " filename=" + cacheFile);
      return null;
    }
    return BitmapFactory.decodeFile(cacheFile.getAbsolutePath());
  }

  /**
   * @param url The URL of the image that will be retrieved from the cache.
   * @return The cached bitmap or null if it was not found.
   */
  private Bitmap getBitmapFromCache(String url) {
    // First try the hard reference cache
    synchronized (sHardBitmapCache) {
      final Bitmap bitmap = sHardBitmapCache.get(url);
      if (bitmap != null) {
        // Bitmap found in hard cache
        // Move element to first position, so that it is removed last
        sHardBitmapCache.remove(url);
        sHardBitmapCache.put(url, bitmap);
        return bitmap;
      }
    }

    // Then try the soft reference cache
    SoftReference<Bitmap> bitmapReference = sSoftBitmapCache.get(url);
    if (bitmapReference != null) {
      final Bitmap bitmap = bitmapReference.get();
      if (bitmap != null) {
        // Bitmap found in soft cache
        return bitmap;
      }
      // Soft reference has been Garbage Collected
      sSoftBitmapCache.remove(url);
    }

    return null;
  }

  /**
   * Clears the image cache used internally to improve performance. Note that for memory efficiency
   * reasons, the cache will automatically be cleared after a certain inactivity delay.
   */
  public void clearCache() {
    sHardBitmapCache.clear();
    sSoftBitmapCache.clear();
  }

}
