// "Therefore those skilled at the unorthodox
// are infinite as heaven and earth,
// inexhaustible as the great rivers.
// When they come to an end,
// they begin again,
// like the days and months;
// they die and are reborn,
// like the four seasons."
//
// - Sun Tsu,
// "The Art of War"

package com.theartofdev.edmodo.cropper;

import android.content.Context;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.AsyncTask;

import java.lang.ref.WeakReference;

/**
 * Task to crop bitmap asynchronously from the UI thread.
 */
final class BitmapCroppingWorkerTask
        extends AsyncTask<Void, Void, BitmapCroppingWorkerTask.Result> {

    // region: Fields and Consts

    /**
     * Use a WeakReference to ensure the ImageView can be garbage collected
     */
    private final WeakReference<CropImageView> mCropImageViewReference;

    /**
     * the bitmap to crop
     */
    private final Bitmap mBitmap;

    /**
     * The Android URI of the image to load
     */
    private final Uri mUri;

    /**
     * The context of the crop image view widget used for loading of bitmap by Android URI
     */
    private final Context mContext;

    /**
     * Required cropping 4 points (x0,y0,x1,y1,x2,y2,x3,y3)
     */
    private final float[] mCropPoints;

    /**
     * Degrees the image was rotated after loading
     */
    private final int mDegreesRotated;

    /**
     * the original width of the image to be cropped (for image loaded from URI)
     */
    private final int mOrgWidth;

    /**
     * the original height of the image to be cropped (for image loaded from URI)
     */
    private final int mOrgHeight;

    /**
     * is there is fixed aspect ratio for the crop rectangle
     */
    private final boolean mFixAspectRatio;

    /**
     * the X aspect ration of the crop rectangle
     */
    private final int mAspectRatioX;

    /**
     * the Y aspect ration of the crop rectangle
     */
    private final int mAspectRatioY;

    /**
     * required width of the cropping image
     */
    private final int mReqWidth;

    /**
     * required height of the cropping image
     */
    private final int mReqHeight;

    /**
     * is the image flipped horizontally
     */
    private final boolean mFlipHorizontally;

    /**
     * is the image flipped vertically
     */
    private final boolean mFlipVertically;

    /**
     * The option to handle requested width/height
     */
    private final CropImageView.RequestSizeOptions mReqSizeOptions;

    /**
     * the Android Uri to save the cropped image to
     */
    private final Uri mSaveUri;

    /**
     * the compression format to use when writing the image
     */
    private final Bitmap.CompressFormat mSaveCompressFormat;

    /**
     * the quality (if applicable) to use when writing the image (0 - 100)
     */
    private final int mSaveCompressQuality;
    // endregion

    BitmapCroppingWorkerTask(
            CropImageView cropImageView,
            Bitmap bitmap,
            float[] cropPoints,
            int degreesRotated,
            boolean fixAspectRatio,
            int aspectRatioX,
            int aspectRatioY,
            int reqWidth,
            int reqHeight,
            boolean flipHorizontally,
            boolean flipVertically,
            CropImageView.RequestSizeOptions options,
            Uri saveUri,
            Bitmap.CompressFormat saveCompressFormat,
            int saveCompressQuality) {

        mCropImageViewReference = new WeakReference<>(cropImageView);
        mContext = cropImageView.getContext();
        mBitmap = bitmap;
        mCropPoints = cropPoints;
        mUri = null;
        mDegreesRotated = degreesRotated;
        mFixAspectRatio = fixAspectRatio;
        mAspectRatioX = aspectRatioX;
        mAspectRatioY = aspectRatioY;
        mReqWidth = reqWidth;
        mReqHeight = reqHeight;
        mFlipHorizontally = flipHorizontally;
        mFlipVertically = flipVertically;
        mReqSizeOptions = options;
        mSaveUri = saveUri;
        mSaveCompressFormat = saveCompressFormat;
        mSaveCompressQuality = saveCompressQuality;
        mOrgWidth = 0;
        mOrgHeight = 0;
    }

    BitmapCroppingWorkerTask(
            CropImageView cropImageView,
            Uri uri,
            float[] cropPoints,
            int degreesRotated,
            int orgWidth,
            int orgHeight,
            boolean fixAspectRatio,
            int aspectRatioX,
            int aspectRatioY,
            int reqWidth,
            int reqHeight,
            boolean flipHorizontally,
            boolean flipVertically,
            CropImageView.RequestSizeOptions options,
            Uri saveUri,
            Bitmap.CompressFormat saveCompressFormat,
            int saveCompressQuality) {

        mCropImageViewReference = new WeakReference<>(cropImageView);
        mContext = cropImageView.getContext();
        mUri = uri;
        mCropPoints = cropPoints;
        mDegreesRotated = degreesRotated;
        mFixAspectRatio = fixAspectRatio;
        mAspectRatioX = aspectRatioX;
        mAspectRatioY = aspectRatioY;
        mOrgWidth = orgWidth;
        mOrgHeight = orgHeight;
        mReqWidth = reqWidth;
        mReqHeight = reqHeight;
        mFlipHorizontally = flipHorizontally;
        mFlipVertically = flipVertically;
        mReqSizeOptions = options;
        mSaveUri = saveUri;
        mSaveCompressFormat = saveCompressFormat;
        mSaveCompressQuality = saveCompressQuality;
        mBitmap = null;
    }

    /**
     * The Android URI that this task is currently loading.
     */
    public Uri getUri() {
        return mUri;
    }

    /**
     * Crop image in background.
     *
     * @param params ignored
     * @return the decoded bitmap data
     */
    @Override
    protected BitmapCroppingWorkerTask.Result doInBackground(Void... params) {
        try {
            if (!isCancelled()) {

                BitmapUtils.BitmapSampled bitmapSampled;
                if (mUri != null) {
                    bitmapSampled =
                            BitmapUtils.cropBitmap(
                                    mContext,
                                    mUri,
                                    mCropPoints,
                                    mDegreesRotated,
                                    mOrgWidth,
                                    mOrgHeight,
                                    mFixAspectRatio,
                                    mAspectRatioX,
                                    mAspectRatioY,
                                    mReqWidth,
                                    mReqHeight,
                                    mFlipHorizontally,
                                    mFlipVertically);
                } else if (mBitmap != null) {
                    bitmapSampled =
                            BitmapUtils.cropBitmapObjectHandleOOM(
                                    mBitmap,
                                    mCropPoints,
                                    mDegreesRotated,
                                    mFixAspectRatio,
                                    mAspectRatioX,
                                    mAspectRatioY,
                                    mFlipHorizontally,
                                    mFlipVertically);
                } else {
                    return new Result((Bitmap) null, 1);
                }

                Bitmap bitmap =
                        BitmapUtils.resizeBitmap(bitmapSampled.bitmap, mReqWidth, mReqHeight, mReqSizeOptions);

                if (mSaveUri == null) {
                    return new Result(bitmap, bitmapSampled.sampleSize);
                } else {
                    BitmapUtils.writeBitmapToUri(
                            mContext, bitmap, mSaveUri, mSaveCompressFormat, mSaveCompressQuality);
                    if (bitmap != null) {
                        bitmap.recycle();
                    }
                    return new Result(mSaveUri, bitmapSampled.sampleSize);
                }
            }
            return null;
        } catch (Exception e) {
            return new Result(e, mSaveUri != null);
        }
    }

    /**
     * Once complete, see if ImageView is still around and set bitmap.
     *
     * @param result the result of bitmap cropping
     */
    @Override
    protected void onPostExecute(Result result) {
        if (result != null) {
            boolean completeCalled = false;
            if (!isCancelled()) {
                CropImageView cropImageView = mCropImageViewReference.get();
                if (cropImageView != null) {
                    completeCalled = true;
                    cropImageView.onImageCroppingAsyncComplete(result);
                }
            }
            if (!completeCalled && result.bitmap != null) {
                // fast release of unused bitmap
                result.bitmap.recycle();
            }
        }
    }

    // region: Inner class: Result

    /**
     * The result of BitmapCroppingWorkerTask async loading.
     */
    static final class Result {

        /**
         * The cropped bitmap
         */
        public final Bitmap bitmap;

        /**
         * The saved cropped bitmap uri
         */
        public final Uri uri;

        /**
         * The error that occurred during async bitmap cropping.
         */
        final Exception error;

        /**
         * is the cropping request was to get a bitmap or to save it to uri
         */
        final boolean isSave;

        /**
         * sample size used creating the crop bitmap to lower its size
         */
        final int sampleSize;

        Result(Bitmap bitmap, int sampleSize) {
            this.bitmap = bitmap;
            this.uri = null;
            this.error = null;
            this.isSave = false;
            this.sampleSize = sampleSize;
        }

        Result(Uri uri, int sampleSize) {
            this.bitmap = null;
            this.uri = uri;
            this.error = null;
            this.isSave = true;
            this.sampleSize = sampleSize;
        }

        Result(Exception error, boolean isSave) {
            this.bitmap = null;
            this.uri = null;
            this.error = error;
            this.isSave = isSave;
            this.sampleSize = 1;
        }
    }
    // endregion
}
