package com.gigabytedevelopersinc.app.explorer.misc;

import android.content.ContentResolver;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.graphics.Point;
import android.graphics.Rect;
import android.media.ThumbnailUtils;
import android.net.Uri;
import android.provider.MediaStore;
import android.support.media.ExifInterface;
import android.util.Base64;
import android.util.Log;
import android.widget.ImageView;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.regex.Pattern;


public class ImageUtils {

    private static final String TAG = "ImageUtils";
    private static final String BASE64_URI_PREFIX = "base64,";
    private static final Pattern BASE64_IMAGE_URI_PATTERN = Pattern.compile("^(?:.*;)?base64,.*");
    /**
     * Returns the largest power-of-two divisor for use in downscaling a bitmap
     * that will not result in the scaling past the desired dimensions.
     *
     * @param actualWidth Actual width of the bitmap
     * @param actualHeight Actual height of the bitmap
     * @param desiredWidth Desired width of the bitmap
     * @param desiredHeight Desired height of the bitmap
     */
    // Visible for testing.
    public static int findBestSampleSize(
            int actualWidth, int actualHeight, int desiredWidth, int desiredHeight) {
        double wr = (double) actualWidth / desiredWidth;
        double hr = (double) actualHeight / desiredHeight;
        double ratio = Math.min(wr, hr);
        float n = 1.0f;
        while ((n * 2) <= ratio) {
            n *= 2;
        }

        return (int) n;
    }

    public static Bitmap getThumbnail(String path, String mimeType, int mMaxWidth, int mMaxHeight) {
        try {
            final String typeOnly = mimeType.split("/")[0];
            if ("image".equals(typeOnly)) {
                return getImageThumbnail(path, mMaxWidth, mMaxHeight);
            } else if ("video".equals(typeOnly)) {
                return getVideoThumbnail(path, mMaxWidth, mMaxHeight);
            } else {
                return null;
            }
        } catch (OutOfMemoryError e) {
            return null;
        }
    }

    public static Bitmap getThumbnail(ContentResolver resolver, Uri imageUri, int mMaxWidth, int mMaxHeight) {
        try {
            return getContentThumbnail(resolver, imageUri, mMaxWidth, mMaxHeight);
        } catch (OutOfMemoryError e) {
            return null;
        }
    }

    public static Bitmap getImageThumbnail(String path, int mMaxWidth, int mMaxHeight){
        Bitmap.Config mDecodeConfig = Bitmap.Config.RGB_565;
        ImageView.ScaleType mScaleType = ImageView.ScaleType.CENTER_CROP;

        File bitmapFile = new File(path);
        Bitmap bitmap = null;

        if (!bitmapFile.exists() || !bitmapFile.isFile()) {
            return bitmap;
        }

        BitmapFactory.Options decodeOptions = new BitmapFactory.Options();
        decodeOptions.inInputShareable = true;
        decodeOptions.inPurgeable = true;
        decodeOptions.inPreferredConfig = mDecodeConfig;
        if (mMaxWidth == 0 && mMaxHeight == 0) {

            bitmap = BitmapFactory.decodeFile(bitmapFile.getAbsolutePath(), decodeOptions);
        } else {
            // If we have to resize this image, first get the natural bounds.
            decodeOptions.inJustDecodeBounds = true;
            BitmapFactory.decodeFile(bitmapFile.getAbsolutePath(), decodeOptions);
            int actualWidth = decodeOptions.outWidth;
            int actualHeight = decodeOptions.outHeight;

            // Then compute the dimensions we would ideally like to decode to.
            int desiredWidth = getResizedDimension(mMaxWidth, mMaxHeight,
                    actualWidth, actualHeight, mScaleType);
            int desiredHeight = getResizedDimension(mMaxHeight, mMaxWidth,
                    actualHeight, actualWidth, mScaleType);

            // Decode to the nearest power of two scaling factor.
            decodeOptions.inJustDecodeBounds = false;
            decodeOptions.inSampleSize = ImageUtils.findBestSampleSize(actualWidth, actualHeight, desiredWidth, desiredHeight);
            Bitmap tempBitmap = BitmapFactory.decodeFile(bitmapFile.getAbsolutePath(), decodeOptions);
            // If necessary, scale down to the maximal acceptable size.
            if (tempBitmap != null
                    && (tempBitmap.getWidth() > desiredWidth || tempBitmap.getHeight() > desiredHeight)) {
                bitmap = Bitmap.createScaledBitmap(tempBitmap, desiredWidth,
                        desiredHeight, true);
                tempBitmap.recycle();
            } else {
                bitmap = tempBitmap;
            }

        }
        return bitmap;
    }

    public static Bitmap getVideoThumbnail(String path, int mMaxWidth, int mMaxHeight){
        Bitmap.Config mDecodeConfig = Bitmap.Config.RGB_565;
        ImageView.ScaleType mScaleType = ImageView.ScaleType.CENTER_CROP;

        File bitmapFile = new File(path);
        Bitmap bitmap = null;

        if (!bitmapFile.exists() || !bitmapFile.isFile()) {
            return bitmap;
        }

        BitmapFactory.Options decodeOptions = new BitmapFactory.Options();
        decodeOptions.inInputShareable = true;
        decodeOptions.inPurgeable = true;
        decodeOptions.inPreferredConfig = mDecodeConfig;
        if (mMaxWidth == 0 && mMaxHeight == 0) {

            bitmap = getVideoFrame(bitmapFile.getAbsolutePath());
        } else {
            // If we have to resize this image, first get the natural bounds.
            decodeOptions.inJustDecodeBounds = true;
            //BitmapFactory.decodeFile(bitmapFile.getAbsolutePath(), decodeOptions);
            int actualWidth = decodeOptions.outWidth;
            int actualHeight = decodeOptions.outHeight;

            // Then compute the dimensions we would ideally like to decode to.
            int desiredWidth = getResizedDimension(mMaxWidth, mMaxHeight,
                    actualWidth, actualHeight, mScaleType);
            int desiredHeight = getResizedDimension(mMaxHeight, mMaxWidth,
                    actualHeight, actualWidth, mScaleType);

            // Decode to the nearest power of two scaling factor.
            decodeOptions.inJustDecodeBounds = false;
            decodeOptions.inSampleSize = ImageUtils.findBestSampleSize(actualWidth, actualHeight, desiredWidth, desiredHeight);
            Bitmap tempBitmap = getVideoFrame(bitmapFile.getAbsolutePath());
            // If necessary, scale down to the maximal acceptable size.
            if (tempBitmap != null
                    && (tempBitmap.getWidth() > desiredWidth || tempBitmap.getHeight() > desiredHeight)) {
                bitmap = Bitmap.createScaledBitmap(tempBitmap, desiredWidth,
                        desiredHeight, true);
                tempBitmap.recycle();
            } else {
                bitmap = tempBitmap;
            }
        }
        return bitmap;
    }

    private static Bitmap getVideoFrame(String path) {
        return ThumbnailUtils.createVideoThumbnail(path, MediaStore.Images.Thumbnails.MINI_KIND);
    }

    /**
     * The real guts of parseNetworkResponse. Broken out for readability.
     *
     * This version is for reading a Bitmap from resource
     */
    private static Bitmap getContentThumbnail(ContentResolver resolver, Uri imageUri, int mMaxWidth, int mMaxHeight) {

        Bitmap.Config mDecodeConfig = Bitmap.Config.RGB_565;
        ImageView.ScaleType mScaleType = ImageView.ScaleType.CENTER_CROP;

        Bitmap bitmap = null;

        BitmapFactory.Options decodeOptions = new BitmapFactory.Options();
        decodeOptions.inInputShareable = true;
        decodeOptions.inPurgeable = true;
        decodeOptions.inPreferredConfig = mDecodeConfig;

        if (mMaxWidth == 0 && mMaxHeight == 0) {
            bitmap = ImageUtils.decodeStream(resolver, imageUri, decodeOptions);
        } else {
            // If we have to resize this image, first get the natural bounds.
            decodeOptions.inJustDecodeBounds = true;
            ImageUtils.decodeStream(resolver, imageUri, decodeOptions);
            int actualWidth = decodeOptions.outWidth;
            int actualHeight = decodeOptions.outHeight;

            // Then compute the dimensions we would ideally like to decode to.
            int desiredWidth = getResizedDimension(mMaxWidth, mMaxHeight,
                    actualWidth, actualHeight, mScaleType);
            int desiredHeight = getResizedDimension(mMaxHeight, mMaxWidth,
                    actualHeight, actualWidth, mScaleType);

            // Decode to the nearest power of two scaling factor.
            decodeOptions.inJustDecodeBounds = false;

            decodeOptions.inSampleSize = ImageUtils.findBestSampleSize(actualWidth, actualHeight, desiredWidth, desiredHeight);
            Bitmap tempBitmap = ImageUtils.decodeStream(resolver, imageUri, decodeOptions);
            // If necessary, scale down to the maximal acceptable size.
            if (tempBitmap != null && (tempBitmap.getWidth() > desiredWidth || tempBitmap.getHeight() > desiredHeight)) {
                bitmap = Bitmap.createScaledBitmap(tempBitmap, desiredWidth, desiredHeight, true);
                tempBitmap.recycle();
            } else {
                bitmap = tempBitmap;
            }
        }
        return bitmap;
    }

    /**
     * Scales one side of a rectangle to fit aspect ratio.
     *
     * @param maxPrimary Maximum size of the primary dimension (i.e. width for
     *        max width), or zero to maintain aspect ratio with secondary
     *        dimension
     * @param maxSecondary Maximum size of the secondary dimension, or zero to
     *        maintain aspect ratio with primary dimension
     * @param actualPrimary Actual size of the primary dimension
     * @param actualSecondary Actual size of the secondary dimension
     * @param scaleType The ScaleType used to calculate the needed image size.
     */
    private static int getResizedDimension(int maxPrimary, int maxSecondary, int actualPrimary,
                                           int actualSecondary, ImageView.ScaleType scaleType) {
        // If no dominant value at all, just return the actual.
        if ((maxPrimary == 0) && (maxSecondary == 0)) {
            return actualPrimary;
        }
        // If ScaleType.FIT_XY fill the whole rectangle, ignore ratio.
        if (scaleType == ImageView.ScaleType.FIT_XY) {
            if (maxPrimary == 0) {
                return actualPrimary;
            }
            return maxPrimary;
        }
        // If primary is unspecified, scale primary to match secondary's scaling ratio.
        if (maxPrimary == 0) {
            double ratio = (double) maxSecondary / (double) actualSecondary;
            return (int) (actualPrimary * ratio);
        }
        if (maxSecondary == 0) {
            return maxPrimary;
        }
        double ratio = (double) actualSecondary / (double) actualPrimary;
        int resized = maxPrimary;
        // If ScaleType.CENTER_CROP fill the whole rectangle, preserve aspect ratio.
        if (scaleType == ImageView.ScaleType.CENTER_CROP) {
            if ((resized * ratio) < maxSecondary) {
                resized = (int) (maxSecondary / ratio);
            }
            return resized;
        }
        if ((resized * ratio) > maxSecondary) {
            resized = (int) (maxSecondary / ratio);
        }
        return resized;
    }

    /**
     * Create a bitmap from a local URI
     *
     * @param resolver The ContentResolver
     * @param uri      The local URI
     * @param maxSize  The maximum size (either width or height)
     * @return The new bitmap or null
     */
    public static Bitmap decodeStream(final ContentResolver resolver, final Uri uri,
                                      final int maxSize) {
        Bitmap result = null;
        final InputStreamFactory factory = createInputStreamFactory(resolver, uri);
        try {
            final Point bounds = getImageBounds(factory);
            if (bounds == null) {
                return result;
            }

            final BitmapFactory.Options opts = new BitmapFactory.Options();
            opts.inSampleSize = Math.max(bounds.x / maxSize, bounds.y / maxSize);
            result = decodeStream(factory, null, opts);
            return result;

        } catch (FileNotFoundException exception) {
            // Do nothing - the photo will appear to be missing
        } catch (IOException exception) {
        } catch (IllegalArgumentException exception) {
            // Do nothing - the photo will appear to be missing
        } catch (SecurityException exception) {
        }
        return result;
    }

    /**
     * Create a bitmap from a local URI
     *
     * @param resolver The ContentResolver
     * @param uri      The local URI
     * @return The new bitmap or null
     */
    public static Bitmap decodeStream(final ContentResolver resolver, final Uri uri,
                                      BitmapFactory.Options opts) {
        Bitmap result = null;
        final InputStreamFactory factory = createInputStreamFactory(resolver, uri);
        try {
            result = decodeStream(factory, null, opts);
            return result;

        } catch (FileNotFoundException exception) {
            // Do nothing - the photo will appear to be missing
        } catch (IllegalArgumentException exception) {
            // Do nothing - the photo will appear to be missing
        } catch (SecurityException exception) {
        }
        return result;
    }

    /**
     * Wrapper around {@link BitmapFactory#decodeStream(InputStream, Rect,
     * BitmapFactory.Options)} that returns {@code null} on {@link
     * OutOfMemoryError}.
     *
     * @param is The input stream that holds the raw data to be decoded into a
     *           bitmap.
     * @param outPadding If not null, return the padding rect for the bitmap if
     *                   it exists, otherwise set padding to [-1,-1,-1,-1]. If
     *                   no bitmap is returned (null) then padding is
     *                   unchanged.
     * @param opts null-ok; Options that control downsampling and whether the
     *             image should be completely decoded, or just is size returned.
     * @return The decoded bitmap, or null if the image data could not be
     *         decoded, or, if opts is non-null, if opts requested only the
     *         size be returned (in opts.outWidth and opts.outHeight)
     */
    public static Bitmap decodeStream(InputStream is, Rect outPadding, BitmapFactory.Options opts) {
        try {
            return BitmapFactory.decodeStream(is, outPadding, opts);
        } catch (OutOfMemoryError oome) {
            Log.e(TAG, "ImageUtils#decodeStream(InputStream, Rect, Options) threw an OOME", oome);
            return null;
        }
    }

    /**
     * Wrapper around {@link BitmapFactory#decodeStream(InputStream, Rect,
     * BitmapFactory.Options)} that returns {@code null} on {@link
     * OutOfMemoryError}.
     *
     * @param factory    Used to create input streams that holds the raw data to be decoded into a
     *                   bitmap.
     * @param outPadding If not null, return the padding rect for the bitmap if
     *                   it exists, otherwise set padding to [-1,-1,-1,-1]. If
     *                   no bitmap is returned (null) then padding is
     *                   unchanged.
     * @param opts       null-ok; Options that control downsampling and whether the
     *                   image should be completely decoded, or just is size returned.
     * @return The decoded bitmap, or null if the image data could not be
     * decoded, or, if opts is non-null, if opts requested only the
     * size be returned (in opts.outWidth and opts.outHeight)
     */
    public static Bitmap decodeStream(final InputStreamFactory factory, final Rect outPadding,
                                      final BitmapFactory.Options opts) throws FileNotFoundException {
        InputStream is = null;
        try {
            // Determine the orientation for this image
            is = factory.createInputStream();
            final ExifInterface exif = new ExifInterface(is);
            final int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, -1);
            is.close();

            // Decode the bitmap
            is = factory.createInputStream();
            final Bitmap originalBitmap = BitmapFactory.decodeStream(is, outPadding, opts);

            if (is != null && originalBitmap == null && !opts.inJustDecodeBounds) {
                Log.w(TAG, "ImageUtils#decodeStream(InputStream, Rect, Options): "
                        + "Image bytes cannot be decoded into a Bitmap");
                throw new UnsupportedOperationException(
                        "Image bytes cannot be decoded into a Bitmap.");
            }

            // Rotate the Bitmap based on the orientation
            if (originalBitmap != null && orientation != 0) {
                final Matrix matrix = new Matrix();
                matrix.postRotate(orientation);
                return Bitmap.createBitmap(originalBitmap, 0, 0, originalBitmap.getWidth(),
                        originalBitmap.getHeight(), matrix, true);
            }
            return originalBitmap;
        } catch (OutOfMemoryError oome) {
            Log.e(TAG, "ImageUtils#decodeStream(InputStream, Rect, Options) threw an OOME", oome);
            return null;
        } catch (IOException ioe) {
            Log.e(TAG, "ImageUtils#decodeStream(InputStream, Rect, Options) threw an IOE", ioe);
            return null;
        } finally {
            if (is != null) {
                try {
                    is.close();
                } catch (IOException e) {
                    // Do nothing
                }
            }
        }
    }

    /**
     * Gets the image bounds
     *
     * @param factory Used to create the InputStream.
     *
     * @return The image bounds
     */
    public static Point getImageBounds(final InputStreamFactory factory)
            throws IOException {
        final BitmapFactory.Options opts = new BitmapFactory.Options();
        opts.inJustDecodeBounds = true;
        decodeStream(factory, null, opts);

        return new Point(opts.outWidth, opts.outHeight);
    }


    public static InputStreamFactory createInputStreamFactory(final ContentResolver resolver,
                                                              final Uri uri) {
        final String scheme = uri.getScheme();
        if ("data".equals(scheme)) {
            return new DataInputStreamFactory(resolver, uri);
        }
        return new BaseInputStreamFactory(resolver, uri);
    }

    /**
     * Utility class for when an InputStream needs to be read multiple times. For example, one pass
     * may load EXIF orientation, and the second pass may do the actual Bitmap decode.
     */
    public interface InputStreamFactory {

        /**
         * Create a new InputStream. The caller of this method must be able to read the input
         * stream starting from the beginning.
         * @return
         */
        InputStream createInputStream() throws FileNotFoundException;
    }

    private static class BaseInputStreamFactory implements InputStreamFactory {
        protected final ContentResolver mResolver;
        protected final Uri mUri;

        public BaseInputStreamFactory(final ContentResolver resolver, final Uri uri) {
            mResolver = resolver;
            mUri = uri;
        }

        @Override
        public InputStream createInputStream() throws FileNotFoundException {
            return mResolver.openInputStream(mUri);
        }
    }

    private static class DataInputStreamFactory extends BaseInputStreamFactory {
        private byte[] mData;

        public DataInputStreamFactory(final ContentResolver resolver, final Uri uri) {
            super(resolver, uri);
        }

        @Override
        public InputStream createInputStream() throws FileNotFoundException {
            if (mData == null) {
                mData = parseDataUri(mUri);
                if (mData == null) {
                    return super.createInputStream();
                }
            }
            return new ByteArrayInputStream(mData);
        }

        private byte[] parseDataUri(final Uri uri) {
            final String ssp = uri.getSchemeSpecificPart();
            try {
                if (ssp.startsWith(BASE64_URI_PREFIX)) {
                    final String base64 = ssp.substring(BASE64_URI_PREFIX.length());
                    return Base64.decode(base64, Base64.URL_SAFE);
                } else if (BASE64_IMAGE_URI_PATTERN.matcher(ssp).matches()){
                    final String base64 = ssp.substring(
                            ssp.indexOf(BASE64_URI_PREFIX) + BASE64_URI_PREFIX.length());
                    return Base64.decode(base64, Base64.DEFAULT);
                } else {
                    return null;
                }
            } catch (IllegalArgumentException ex) {
                Log.e(TAG, "Mailformed data URI: " + ex);
                return null;
            }
        }
    }
}