package com.amirarcane.recentimages.thumbnailOptions;

import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Context;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.BitmapFactory.Options;
import android.graphics.Matrix;
import android.media.ExifInterface;
import android.net.Uri;
import android.os.Handler;
import android.provider.MediaStore;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CursorAdapter;
import android.widget.ImageView;

import com.amirarcane.recentimages.R;
import com.jess.ui.TwoWayAbsListView;

import java.io.IOException;
import java.lang.ref.SoftReference;
import java.util.LinkedList;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class ImageAdapter extends CursorAdapter {
	private static final String[] IMAGE_PROJECTION = {
			MediaStore.Images.ImageColumns._ID,
			MediaStore.Images.ImageColumns.DISPLAY_NAME,
	};

	public static final int IMAGE_ID_COLUMN = 0;
	public static final int IMAGE_NAME_COLUMN = 1;

	public static final boolean DEBUG = false;

	private static final String TAG = "ImageAdapter";

	public static float IMAGE_WIDTH = 70;
	public static float IMAGE_HEIGHT = 70;
	public static float IMAGE_PADDING = 0;

	private static final Map<Long, SoftReference<ReplaceableBitmapDrawable>> sImageCache =
			new ConcurrentHashMap<Long, SoftReference<ReplaceableBitmapDrawable>>();

	private static Options sBitmapOptions = new Options();

	private final Context mContext;
	private Bitmap mDefaultBitmap;
	private final ContentResolver mContentResolver;
	private final Handler mHandler;
	private float mScale;
	private int mImageWidth;
	private int mImageHeight;
	private int mImagePadding;

	public ImageView.ScaleType SCALE_TYPE = ImageView.ScaleType.CENTER_CROP;
	public static int IN_SAMPLE_SIZE = 3;
	public static int KIND = 1;

	public static int DRAWABLE = R.drawable.spinner_black_76;

	public ImageAdapter(Context context, Cursor c) {
		this(context, c, true);
	}

	public ImageAdapter(Context context, Cursor c, boolean autoRequery) {
		super(context, c, autoRequery);
		mContext = context;
		init(c);
		mContentResolver = context.getContentResolver();
		mHandler = new Handler();
	}

	private void init(Cursor c) {
		mDefaultBitmap = BitmapFactory.decodeResource(mContext.getResources(), DRAWABLE);

		mScale = mContext.getResources().getDisplayMetrics().density;
		mImageWidth = (int) (IMAGE_WIDTH * mScale);
		mImageHeight = (int) (IMAGE_HEIGHT * mScale);
		mImagePadding = (int) (IMAGE_PADDING * mScale);
		sBitmapOptions.inSampleSize = IN_SAMPLE_SIZE;
	}

	@Override
	public int getItemViewType(int position) {
		return 0;
	}

	@Override
	public int getViewTypeCount() {
		return 1;
	}

	@Override
	public void bindView(View view, Context context, Cursor cursor) {
		int id = cursor.getInt(IMAGE_ID_COLUMN);
		((ImageView) view).setImageDrawable(getCachedThumbnailAsync(
				ContentUris.withAppendedId(MediaStore.Images.Thumbnails.EXTERNAL_CONTENT_URI, id)));
	}

	@Override
	public View newView(Context context, Cursor cursor, ViewGroup parent) {
		ImageView imageView = new BetterImageView(mContext.getApplicationContext());
		imageView.setLayoutParams(new TwoWayAbsListView.LayoutParams(mImageWidth, mImageHeight));
		imageView.setPadding(mImagePadding, mImagePadding, mImagePadding, mImagePadding);
		imageView.setScaleType(SCALE_TYPE);
		return imageView;
	}

	public void cleanup() {
		cleanupCache();
	}


	private static Bitmap loadThumbnail(ContentResolver cr, Uri uri) {

		int id = (int) ContentUris.parseId(uri);
		int orientation = getOrientation(cr, id);
		Matrix matrix = new Matrix();
		matrix.postRotate(orientation);
		Log.d("Orientation", String.valueOf(orientation));
		Bitmap bitmap = MediaStore.Images.Thumbnails.getThumbnail(
				cr, id, KIND, sBitmapOptions);
		bitmap = crop(bitmap, matrix);
		return bitmap;
	}

	private static int getOrientation(ContentResolver cr, int id) {

		String photoID = String.valueOf(id);

		Cursor cursor = cr.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
				new String[] {MediaStore.Images.Media.ORIENTATION}, MediaStore.Images.Media._ID + "=?",
				new String[] {"" + photoID}, null);
		int orientation = -1;

		if (cursor.getCount() != 1) {
			return -1;
		}

		if (cursor.moveToFirst())
		{
			orientation = cursor.getInt(0);
		}
		cursor.close();
		return orientation;
	}

	private static Bitmap crop(Bitmap srcBmp, Matrix matrix) {
		Bitmap dstBmp;
		if (srcBmp.getWidth() >= srcBmp.getHeight()) {

			dstBmp = Bitmap.createBitmap(
					srcBmp,
					srcBmp.getWidth() / 2 - srcBmp.getHeight() / 2,
					0,
					srcBmp.getHeight(),
					srcBmp.getHeight(),
					matrix,
					true
			);

		} else {

			dstBmp = Bitmap.createBitmap(
					srcBmp,
					0,
					srcBmp.getHeight() / 2 - srcBmp.getWidth() / 2,
					srcBmp.getWidth(),
					srcBmp.getWidth(),
					matrix,
					true
			);
		}
		return dstBmp;
	}

	/**
	 * Retrieves a drawable from the image cache, identified by the specified id.
	 * If the drawable does not exist in the cache, it is loaded asynchronously and added to the cache.
	 * If the drawable cannot be added to the cache, the specified default drawable is
	 * returned.
	 *
	 * @param uri The uri of the drawable to retrieve
	 * @return The drawable identified by id or defaultImage
	 */
	private ReplaceableBitmapDrawable getCachedThumbnailAsync(Uri uri) {
		ReplaceableBitmapDrawable drawable = null;
		long id = ContentUris.parseId(uri);

		WorkQueue wq = WorkQueue.getInstance();
		synchronized (wq.mQueue) {
			SoftReference<ReplaceableBitmapDrawable> reference = sImageCache.get(id);
			if (reference != null) {
				drawable = reference.get();
			}

			if (drawable == null || !drawable.isLoaded()) {
				drawable = new ReplaceableBitmapDrawable(mDefaultBitmap);
				sImageCache.put(id, new SoftReference<ReplaceableBitmapDrawable>(drawable));
				ImageLoadingArgs args = new ImageLoadingArgs(mContentResolver, mHandler, drawable, uri);
				wq.execute(new ImageLoader(args));

			}
		}

		return drawable;
	}

	/**
	 * Removes all the callbacks from the drawables stored in the memory cache. This
	 * method must be called from the onDestroy() method of any activity using the
	 * cached drawables. Failure to do so will result in the entire activity being
	 * leaked.
	 */
	public static void cleanupCache() {
		for (SoftReference<ReplaceableBitmapDrawable> reference : sImageCache.values()) {
			final ReplaceableBitmapDrawable drawable = reference.get();
			if (drawable != null) drawable.setCallback(null);
		}
	}

	/**
	 * Deletes the specified drawable from the cache.
	 *
	 * @param uri The uri of the drawable to delete from the cache
	 */
	public static void deleteCachedCover(Uri uri) {
		sImageCache.remove(ContentUris.parseId(uri));
	}


	/**
	 * Class to asynchronously perform the loading of the bitmap
	 */
	public static class ImageLoader implements Runnable {
		protected ImageLoadingArgs mArgs = null;

		public ImageLoader(ImageLoadingArgs args) {
			mArgs = args;
		}

		public void run() {
			final Bitmap bitmap = loadThumbnail(mArgs.mContentResolver, mArgs.mUri);
			if (DEBUG) Log.i(TAG, "run() bitmap: " + bitmap);
			if (bitmap != null) {
				final ReplaceableBitmapDrawable d = mArgs.mDrawable;
				if (d != null) {
					mArgs.mHandler.post(new Runnable() {
						public void run() {
							if (DEBUG)
								Log.i(TAG, "ImageLoader.run() - setting the bitmap for uri: " + mArgs.mUri);
							d.setBitmap(bitmap);
						}
					});
				} else {
					Log.e(TAG, "ImageLoader.run() - FastBitmapDrawable is null for uri: " + mArgs.mUri);
				}
			} else {
				Log.e(TAG, "ImageLoader.run() - bitmap is null for uri: " + mArgs.mUri);
			}
		}

		public void cancel() {
			sImageCache.remove(mArgs.mUri);
		}

		@Override
		public boolean equals(Object obj) {
			if (obj != null && obj instanceof ImageLoader && mArgs.mUri != null) {
				return mArgs.mUri.equals(((ImageLoader) obj).mArgs);
			}
			return false;
		}

		@Override
		public int hashCode() {
			return mArgs.mUri.hashCode();
		}

	}


	/**
	 * Class to hold all the parts necessary to load an image
	 */
	public static class ImageLoadingArgs {
		ContentResolver mContentResolver;
		Handler mHandler;
		ReplaceableBitmapDrawable mDrawable;
		Uri mUri;

		/**
		 * @param contentResolver - ContentResolver to use
		 * @param drawable        - FastBitmapDrawable whose underlying bitmap should be replaced with new bitmap
		 * @param uri             - Uri of image location
		 */
		public ImageLoadingArgs(ContentResolver contentResolver, Handler handler,
		                        ReplaceableBitmapDrawable drawable, Uri uri) {
			mContentResolver = contentResolver;
			mHandler = handler;
			mDrawable = drawable;
			mUri = uri;
		}
	}


	public static class WorkQueue {
		private static WorkQueue sInstance = null;
		private static final int NUM_OF_THREADS = 1;
		private static final int MAX_QUEUE_SIZE = 21;

		private final int mNumOfThreads;
		private final PoolWorker[] mThreads;
		protected final LinkedList<ImageLoader> mQueue;

		public static synchronized WorkQueue getInstance() {
			if (sInstance == null) {
				sInstance = new WorkQueue(NUM_OF_THREADS);
			}
			return sInstance;
		}

		private WorkQueue(int nThreads) {
			mNumOfThreads = nThreads;
			mQueue = new LinkedList<ImageLoader>();
			mThreads = new PoolWorker[mNumOfThreads];

			for (int i = 0; i < mNumOfThreads; i++) {
				mThreads[i] = new PoolWorker();
				mThreads[i].start();
			}
		}

		public void execute(ImageLoader r) {
			synchronized (mQueue) {
				mQueue.remove(r);
				if (mQueue.size() > MAX_QUEUE_SIZE) {
					mQueue.removeFirst().cancel();
				}
				mQueue.addLast(r);
				mQueue.notify();
			}
		}

		private class PoolWorker extends Thread {
			private boolean mRunning = true;

			@Override
			public void run() {
				Runnable r;

				while (mRunning) {
					synchronized (mQueue) {
						while (mQueue.isEmpty() && mRunning) {
							try {
								mQueue.wait();
							} catch (InterruptedException ignored) {
								Thread.currentThread().interrupt();
							}
						}

						r = mQueue.removeFirst();
					}

					// If we don't catch RuntimeException,
					// the pool could leak threads
					try {
						r.run();
					} catch (RuntimeException e) {
						Log.e(TAG, "RuntimeException", e);

					}
				}
				Log.i(TAG, "PoolWorker finished");
			}

			public void stopWorker() {
				mRunning = false;
			}
		}
	}
}