// "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 net.realify.lib.androidimagecropper;

import android.content.ContentResolver;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.BitmapRegionDecoder;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.RectF;
import android.net.Uri;
import androidx.exifinterface.media.ExifInterface;
import android.util.Log;
import android.util.Pair;

import java.io.Closeable;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.ref.WeakReference;

import javax.microedition.khronos.egl.EGL10;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.egl.EGLContext;
import javax.microedition.khronos.egl.EGLDisplay;

/** Utility class that deals with operations with an ImageView. */
final class BitmapUtils {

  static final Rect EMPTY_RECT = new Rect();

  static final RectF EMPTY_RECT_F = new RectF();

  /** Reusable rectangle for general internal usage */
  static final RectF RECT = new RectF();

  /** Reusable point for general internal usage */
  static final float[] POINTS = new float[6];

  /** Reusable point for general internal usage */
  static final float[] POINTS2 = new float[6];

  /** Used to know the max texture size allowed to be rendered */
  private static int mMaxTextureSize;

  /** used to save bitmaps during state save and restore so not to reload them. */
  static Pair<String, WeakReference<Bitmap>> mStateBitmap;

  /**
   * Rotate the given image by reading the Exif value of the image (uri).<br>
   * If no rotation is required the image will not be rotated.<br>
   * New bitmap is created and the old one is recycled.
   */
  static RotateBitmapResult rotateBitmapByExif(Bitmap bitmap, Context context, Uri uri) {
    ExifInterface ei = null;
    try {
      InputStream is = context.getContentResolver().openInputStream(uri);
      if (is != null) {
        ei = new ExifInterface(is);
        is.close();
      }
    } catch (Exception ignored) {
    }
    return ei != null ? rotateBitmapByExif(bitmap, ei) : new RotateBitmapResult(bitmap, 0);
  }

  /**
   * Rotate the given image by given Exif value.<br>
   * If no rotation is required the image will not be rotated.<br>
   * New bitmap is created and the old one is recycled.
   */
  static RotateBitmapResult rotateBitmapByExif(Bitmap bitmap, ExifInterface exif) {
    int degrees;
    int orientation =
        exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
    switch (orientation) {
      case ExifInterface.ORIENTATION_ROTATE_90:
        degrees = 90;
        break;
      case ExifInterface.ORIENTATION_ROTATE_180:
        degrees = 180;
        break;
      case ExifInterface.ORIENTATION_ROTATE_270:
        degrees = 270;
        break;
      default:
        degrees = 0;
        break;
    }
    return new RotateBitmapResult(bitmap, degrees);
  }

  /** Decode bitmap from stream using sampling to get bitmap with the requested limit. */
  static BitmapSampled decodeSampledBitmap(Context context, Uri uri, int reqWidth, int reqHeight) {

    try {
      ContentResolver resolver = context.getContentResolver();

      // First decode with inJustDecodeBounds=true to check dimensions
      BitmapFactory.Options options = decodeImageForOption(resolver, uri);

      // Calculate inSampleSize
      options.inSampleSize =
          Math.max(
              calculateInSampleSizeByReqestedSize(
                  options.outWidth, options.outHeight, reqWidth, reqHeight),
              calculateInSampleSizeByMaxTextureSize(options.outWidth, options.outHeight));

      // Decode bitmap with inSampleSize set
      Bitmap bitmap = decodeImage(resolver, uri, options);

      return new BitmapSampled(bitmap, options.inSampleSize);

    } catch (Exception e) {
      throw new RuntimeException(
          "Failed to load sampled bitmap: " + uri + "\r\n" + e.getMessage(), e);
    }
  }

  /**
   * Crop image bitmap from given bitmap using the given points in the original bitmap and the given
   * rotation.<br>
   * if the rotation is not 0,90,180 or 270 degrees then we must first crop a larger area of the
   * image that contains the requires rectangle, rotate and then crop again a sub rectangle.<br>
   * If crop fails due to OOM we scale the cropping image by 0.5 every time it fails until it is
   * small enough.
   */
  static BitmapSampled cropBitmapObjectHandleOOM(
      Bitmap bitmap,
      float[] points,
      int degreesRotated,
      boolean fixAspectRatio,
      int aspectRatioX,
      int aspectRatioY,
      boolean flipHorizontally,
      boolean flipVertically) {
    int scale = 1;
    while (true) {
      try {
        Bitmap cropBitmap =
            cropBitmapObjectWithScale(
                bitmap,
                points,
                degreesRotated,
                fixAspectRatio,
                aspectRatioX,
                aspectRatioY,
                1 / (float) scale,
                flipHorizontally,
                flipVertically);
        return new BitmapSampled(cropBitmap, scale);
      } catch (OutOfMemoryError e) {
        scale *= 2;
        if (scale > 8) {
          throw e;
        }
      }
    }
  }

  /**
   * Crop image bitmap from given bitmap using the given points in the original bitmap and the given
   * rotation.<br>
   * if the rotation is not 0,90,180 or 270 degrees then we must first crop a larger area of the
   * image that contains the requires rectangle, rotate and then crop again a sub rectangle.
   *
   * @param scale how much to scale the cropped image part, use 0.5 to lower the image by half (OOM
   *     handling)
   */
  private static Bitmap cropBitmapObjectWithScale(
      Bitmap bitmap,
      float[] points,
      int degreesRotated,
      boolean fixAspectRatio,
      int aspectRatioX,
      int aspectRatioY,
      float scale,
      boolean flipHorizontally,
      boolean flipVertically) {

    // get the rectangle in original image that contains the required cropped area (larger for non
    // rectangular crop)
    Rect rect =
        getRectFromPoints(
            points,
            bitmap.getWidth(),
            bitmap.getHeight(),
            fixAspectRatio,
            aspectRatioX,
            aspectRatioY);

    // crop and rotate the cropped image in one operation
    Matrix matrix = new Matrix();
    matrix.setRotate(degreesRotated, bitmap.getWidth() / 2, bitmap.getHeight() / 2);
    matrix.postScale(flipHorizontally ? -scale : scale, flipVertically ? -scale : scale);
    Bitmap result =
        Bitmap.createBitmap(bitmap, rect.left, rect.top, rect.width(), rect.height(), matrix, true);

    if (result == bitmap) {
      // corner case when all bitmap is selected, no worth optimizing for it
      result = bitmap.copy(bitmap.getConfig(), false);
    }

    // rotating by 0, 90, 180 or 270 degrees doesn't require extra cropping
    if (degreesRotated % 90 != 0) {

      // extra crop because non rectangular crop cannot be done directly on the image without
      // rotating first
      result =
          cropForRotatedImage(
              result, points, rect, degreesRotated, fixAspectRatio, aspectRatioX, aspectRatioY);
    }

    return result;
  }

  /**
   * Crop image bitmap from URI by decoding it with specific width and height to down-sample if
   * required.<br>
   * Additionally if OOM is thrown try to increase the sampling (2,4,8).
   */
  static BitmapSampled cropBitmap(
      Context context,
      Uri loadedImageUri,
      float[] points,
      int degreesRotated,
      int orgWidth,
      int orgHeight,
      boolean fixAspectRatio,
      int aspectRatioX,
      int aspectRatioY,
      int reqWidth,
      int reqHeight,
      boolean flipHorizontally,
      boolean flipVertically) {
    int sampleMulti = 1;
    while (true) {
      try {
        // if successful, just return the resulting bitmap
        return cropBitmap(
            context,
            loadedImageUri,
            points,
            degreesRotated,
            orgWidth,
            orgHeight,
            fixAspectRatio,
            aspectRatioX,
            aspectRatioY,
            reqWidth,
            reqHeight,
            flipHorizontally,
            flipVertically,
            sampleMulti);
      } catch (OutOfMemoryError e) {
        // if OOM try to increase the sampling to lower the memory usage
        sampleMulti *= 2;
        if (sampleMulti > 16) {
          throw new RuntimeException(
              "Failed to handle OOM by sampling ("
                  + sampleMulti
                  + "): "
                  + loadedImageUri
                  + "\r\n"
                  + e.getMessage(),
              e);
        }
      }
    }
  }

  /** Get left value of the bounding rectangle of the given points. */
  static float getRectLeft(float[] points) {
    return Math.min(Math.min(Math.min(points[0], points[2]), points[4]), points[6]);
  }

  /** Get top value of the bounding rectangle of the given points. */
  static float getRectTop(float[] points) {
    return Math.min(Math.min(Math.min(points[1], points[3]), points[5]), points[7]);
  }

  /** Get right value of the bounding rectangle of the given points. */
  static float getRectRight(float[] points) {
    return Math.max(Math.max(Math.max(points[0], points[2]), points[4]), points[6]);
  }

  /** Get bottom value of the bounding rectangle of the given points. */
  static float getRectBottom(float[] points) {
    return Math.max(Math.max(Math.max(points[1], points[3]), points[5]), points[7]);
  }

  /** Get width of the bounding rectangle of the given points. */
  static float getRectWidth(float[] points) {
    return getRectRight(points) - getRectLeft(points);
  }

  /** Get height of the bounding rectangle of the given points. */
  static float getRectHeight(float[] points) {
    return getRectBottom(points) - getRectTop(points);
  }

  /** Get horizontal center value of the bounding rectangle of the given points. */
  static float getRectCenterX(float[] points) {
    return (getRectRight(points) + getRectLeft(points)) / 2f;
  }

  /** Get vertical center value of the bounding rectangle of the given points. */
  static float getRectCenterY(float[] points) {
    return (getRectBottom(points) + getRectTop(points)) / 2f;
  }

  /**
   * Get a rectangle for the given 4 points (x0,y0,x1,y1,x2,y2,x3,y3) by finding the min/max 2
   * points that contains the given 4 points and is a straight rectangle.
   */
  static Rect getRectFromPoints(
      float[] points,
      int imageWidth,
      int imageHeight,
      boolean fixAspectRatio,
      int aspectRatioX,
      int aspectRatioY) {
    int left = Math.round(Math.max(0, getRectLeft(points)));
    int top = Math.round(Math.max(0, getRectTop(points)));
    int right = Math.round(Math.min(imageWidth, getRectRight(points)));
    int bottom = Math.round(Math.min(imageHeight, getRectBottom(points)));

    Rect rect = new Rect(left, top, right, bottom);
    if (fixAspectRatio) {
      fixRectForAspectRatio(rect, aspectRatioX, aspectRatioY);
    }

    return rect;
  }

  /**
   * Fix the given rectangle if it doesn't confirm to aspect ration rule.<br>
   * Make sure that width and height are equal if 1:1 fixed aspect ratio is requested.
   */
  private static void fixRectForAspectRatio(Rect rect, int aspectRatioX, int aspectRatioY) {
    if (aspectRatioX == aspectRatioY && rect.width() != rect.height()) {
      if (rect.height() > rect.width()) {
        rect.bottom -= rect.height() - rect.width();
      } else {
        rect.right -= rect.width() - rect.height();
      }
    }
  }

  /**
   * Write given bitmap to a temp file. If file already exists no-op as we already saved the file in
   * this session. Uses JPEG 95% compression.
   *
   * @param uri the uri to write the bitmap to, if null
   * @return the uri where the image was saved in, either the given uri or new pointing to temp
   *     file.
   */
  static Uri writeTempStateStoreBitmap(Context context, Bitmap bitmap, Uri uri) {
    try {
      boolean needSave = true;
      if (uri == null) {
        uri =
            Uri.fromFile(
                File.createTempFile("aic_state_store_temp", ".jpg", context.getCacheDir()));
      } else if (new File(uri.getPath()).exists()) {
        needSave = false;
      }
      if (needSave) {
        writeBitmapToUri(context, bitmap, uri, Bitmap.CompressFormat.JPEG, 95);
      }
      return uri;
    } catch (Exception e) {
      Log.w("AIC", "Failed to write bitmap to temp file for image-cropper save instance state", e);
      return null;
    }
  }

  /** Write the given bitmap to the given uri using the given compression. */
  static void writeBitmapToUri(
      Context context,
      Bitmap bitmap,
      Uri uri,
      Bitmap.CompressFormat compressFormat,
      int compressQuality)
      throws FileNotFoundException {
    OutputStream outputStream = null;
    try {
      outputStream = context.getContentResolver().openOutputStream(uri);
      bitmap.compress(compressFormat, compressQuality, outputStream);
    } finally {
      closeSafe(outputStream);
    }
  }

  /** Resize the given bitmap to the given width/height by the given option.<br> */
  static Bitmap resizeBitmap(
      Bitmap bitmap, int reqWidth, int reqHeight, CropImageView.RequestSizeOptions options) {
    try {
      if (reqWidth > 0
          && reqHeight > 0
          && (options == CropImageView.RequestSizeOptions.RESIZE_FIT
              || options == CropImageView.RequestSizeOptions.RESIZE_INSIDE
              || options == CropImageView.RequestSizeOptions.RESIZE_EXACT)) {

        Bitmap resized = null;
        if (options == CropImageView.RequestSizeOptions.RESIZE_EXACT) {
          resized = Bitmap.createScaledBitmap(bitmap, reqWidth, reqHeight, false);
        } else {
          int width = bitmap.getWidth();
          int height = bitmap.getHeight();
          float scale = Math.max(width / (float) reqWidth, height / (float) reqHeight);
          if (scale > 1 || options == CropImageView.RequestSizeOptions.RESIZE_FIT) {
            resized =
                Bitmap.createScaledBitmap(
                    bitmap, (int) (width / scale), (int) (height / scale), false);
          }
        }
        if (resized != null) {
          if (resized != bitmap) {
            bitmap.recycle();
          }
          return resized;
        }
      }
    } catch (Exception e) {
      Log.w("AIC", "Failed to resize cropped image, return bitmap before resize", e);
    }
    return bitmap;
  }

  // region: Private methods

  /**
   * Crop image bitmap from URI by decoding it with specific width and height to down-sample if
   * required.
   *
   * @param orgWidth used to get rectangle from points (handle edge cases to limit rectangle)
   * @param orgHeight used to get rectangle from points (handle edge cases to limit rectangle)
   * @param sampleMulti used to increase the sampling of the image to handle memory issues.
   */
  private static BitmapSampled cropBitmap(
      Context context,
      Uri loadedImageUri,
      float[] points,
      int degreesRotated,
      int orgWidth,
      int orgHeight,
      boolean fixAspectRatio,
      int aspectRatioX,
      int aspectRatioY,
      int reqWidth,
      int reqHeight,
      boolean flipHorizontally,
      boolean flipVertically,
      int sampleMulti) {

    // get the rectangle in original image that contains the required cropped area (larger for non
    // rectangular crop)
    Rect rect =
        getRectFromPoints(points, orgWidth, orgHeight, fixAspectRatio, aspectRatioX, aspectRatioY);

    int width = reqWidth > 0 ? reqWidth : rect.width();
    int height = reqHeight > 0 ? reqHeight : rect.height();

    Bitmap result = null;
    int sampleSize = 1;
    try {
      // decode only the required image from URI, optionally sub-sampling if reqWidth/reqHeight is
      // given.
      BitmapSampled bitmapSampled =
          decodeSampledBitmapRegion(context, loadedImageUri, rect, width, height, sampleMulti);
      result = bitmapSampled.bitmap;
      sampleSize = bitmapSampled.sampleSize;
    } catch (Exception ignored) {
    }

    if (result != null) {
      try {
        // rotate the decoded region by the required amount
        result = rotateAndFlipBitmapInt(result, degreesRotated, flipHorizontally, flipVertically);

        // rotating by 0, 90, 180 or 270 degrees doesn't require extra cropping
        if (degreesRotated % 90 != 0) {

          // extra crop because non rectangular crop cannot be done directly on the image without
          // rotating first
          result =
              cropForRotatedImage(
                  result, points, rect, degreesRotated, fixAspectRatio, aspectRatioX, aspectRatioY);
        }
      } catch (OutOfMemoryError e) {
        if (result != null) {
          result.recycle();
        }
        throw e;
      }
      return new BitmapSampled(result, sampleSize);
    } else {
      // failed to decode region, may be skia issue, try full decode and then crop
      return cropBitmap(
          context,
          loadedImageUri,
          points,
          degreesRotated,
          fixAspectRatio,
          aspectRatioX,
          aspectRatioY,
          sampleMulti,
          rect,
          width,
          height,
          flipHorizontally,
          flipVertically);
    }
  }

  /**
   * Crop bitmap by fully loading the original and then cropping it, fallback in case cropping
   * region failed.
   */
  private static BitmapSampled cropBitmap(
      Context context,
      Uri loadedImageUri,
      float[] points,
      int degreesRotated,
      boolean fixAspectRatio,
      int aspectRatioX,
      int aspectRatioY,
      int sampleMulti,
      Rect rect,
      int width,
      int height,
      boolean flipHorizontally,
      boolean flipVertically) {
    Bitmap result = null;
    int sampleSize;
    try {
      BitmapFactory.Options options = new BitmapFactory.Options();
      options.inSampleSize =
          sampleSize =
              sampleMulti
                  * calculateInSampleSizeByReqestedSize(rect.width(), rect.height(), width, height);

      Bitmap fullBitmap = decodeImage(context.getContentResolver(), loadedImageUri, options);
      if (fullBitmap != null) {
        try {
          // adjust crop points by the sampling because the image is smaller
          float[] points2 = new float[points.length];
          System.arraycopy(points, 0, points2, 0, points.length);
          for (int i = 0; i < points2.length; i++) {
            points2[i] = points2[i] / options.inSampleSize;
          }

          result =
              cropBitmapObjectWithScale(
                  fullBitmap,
                  points2,
                  degreesRotated,
                  fixAspectRatio,
                  aspectRatioX,
                  aspectRatioY,
                  1,
                  flipHorizontally,
                  flipVertically);
        } finally {
          if (result != fullBitmap) {
            fullBitmap.recycle();
          }
        }
      }
    } catch (OutOfMemoryError e) {
      if (result != null) {
        result.recycle();
      }
      throw e;
    } catch (Exception e) {
      throw new RuntimeException(
          "Failed to load sampled bitmap: " + loadedImageUri + "\r\n" + e.getMessage(), e);
    }
    return new BitmapSampled(result, sampleSize);
  }

  /** Decode image from uri using "inJustDecodeBounds" to get the image dimensions. */
  private static BitmapFactory.Options decodeImageForOption(ContentResolver resolver, Uri uri)
      throws FileNotFoundException {
    InputStream stream = null;
    try {
      stream = resolver.openInputStream(uri);
      BitmapFactory.Options options = new BitmapFactory.Options();
      options.inJustDecodeBounds = true;
      BitmapFactory.decodeStream(stream, EMPTY_RECT, options);
      options.inJustDecodeBounds = false;
      return options;
    } finally {
      closeSafe(stream);
    }
  }

  /**
   * Decode image from uri using given "inSampleSize", but if failed due to out-of-memory then raise
   * the inSampleSize until success.
   */
  private static Bitmap decodeImage(
      ContentResolver resolver, Uri uri, BitmapFactory.Options options)
      throws FileNotFoundException {
    do {
      InputStream stream = null;
      try {
        stream = resolver.openInputStream(uri);
        return BitmapFactory.decodeStream(stream, EMPTY_RECT, options);
      } catch (OutOfMemoryError e) {
        options.inSampleSize *= 2;
      } finally {
        closeSafe(stream);
      }
    } while (options.inSampleSize <= 512);
    throw new RuntimeException("Failed to decode image: " + uri);
  }

  /**
   * Decode specific rectangle bitmap from stream using sampling to get bitmap with the requested
   * limit.
   *
   * @param sampleMulti used to increase the sampling of the image to handle memory issues.
   */
  private static BitmapSampled decodeSampledBitmapRegion(
      Context context, Uri uri, Rect rect, int reqWidth, int reqHeight, int sampleMulti) {
    InputStream stream = null;
    BitmapRegionDecoder decoder = null;
    try {
      BitmapFactory.Options options = new BitmapFactory.Options();
      options.inSampleSize =
          sampleMulti
              * calculateInSampleSizeByReqestedSize(
                  rect.width(), rect.height(), reqWidth, reqHeight);

      stream = context.getContentResolver().openInputStream(uri);
      decoder = BitmapRegionDecoder.newInstance(stream, false);
      do {
        try {
          return new BitmapSampled(decoder.decodeRegion(rect, options), options.inSampleSize);
        } catch (OutOfMemoryError e) {
          options.inSampleSize *= 2;
        }
      } while (options.inSampleSize <= 512);
    } catch (Exception e) {
      throw new RuntimeException(
          "Failed to load sampled bitmap: " + uri + "\r\n" + e.getMessage(), e);
    } finally {
      closeSafe(stream);
      if (decoder != null) {
        decoder.recycle();
      }
    }
    return new BitmapSampled(null, 1);
  }

  /**
   * Special crop of bitmap rotated by not stright angle, in this case the original crop bitmap
   * contains parts beyond the required crop area, this method crops the already cropped and rotated
   * bitmap to the final rectangle.<br>
   * Note: rotating by 0, 90, 180 or 270 degrees doesn't require extra cropping.
   */
  private static Bitmap cropForRotatedImage(
      Bitmap bitmap,
      float[] points,
      Rect rect,
      int degreesRotated,
      boolean fixAspectRatio,
      int aspectRatioX,
      int aspectRatioY) {
    if (degreesRotated % 90 != 0) {

      int adjLeft = 0, adjTop = 0, width = 0, height = 0;
      double rads = Math.toRadians(degreesRotated);
      int compareTo =
          degreesRotated < 90 || (degreesRotated > 180 && degreesRotated < 270)
              ? rect.left
              : rect.right;
      for (int i = 0; i < points.length; i += 2) {
        if (points[i] >= compareTo - 1 && points[i] <= compareTo + 1) {
          adjLeft = (int) Math.abs(Math.sin(rads) * (rect.bottom - points[i + 1]));
          adjTop = (int) Math.abs(Math.cos(rads) * (points[i + 1] - rect.top));
          width = (int) Math.abs((points[i + 1] - rect.top) / Math.sin(rads));
          height = (int) Math.abs((rect.bottom - points[i + 1]) / Math.cos(rads));
          break;
        }
      }

      rect.set(adjLeft, adjTop, adjLeft + width, adjTop + height);
      if (fixAspectRatio) {
        fixRectForAspectRatio(rect, aspectRatioX, aspectRatioY);
      }

      Bitmap bitmapTmp = bitmap;
      bitmap = Bitmap.createBitmap(bitmap, rect.left, rect.top, rect.width(), rect.height());
      if (bitmapTmp != bitmap) {
        bitmapTmp.recycle();
      }
    }
    return bitmap;
  }

  /**
   * Calculate the largest inSampleSize value that is a power of 2 and keeps both height and width
   * larger than the requested height and width.
   */
  private static int calculateInSampleSizeByReqestedSize(
      int width, int height, int reqWidth, int reqHeight) {
    int inSampleSize = 1;
    if (height > reqHeight || width > reqWidth) {
      while ((height / 2 / inSampleSize) > reqHeight && (width / 2 / inSampleSize) > reqWidth) {
        inSampleSize *= 2;
      }
    }
    return inSampleSize;
  }

  /**
   * Calculate the largest inSampleSize value that is a power of 2 and keeps both height and width
   * smaller than max texture size allowed for the device.
   */
  private static int calculateInSampleSizeByMaxTextureSize(int width, int height) {
    int inSampleSize = 1;
    if (mMaxTextureSize == 0) {
      mMaxTextureSize = getMaxTextureSize();
    }
    if (mMaxTextureSize > 0) {
      while ((height / inSampleSize) > mMaxTextureSize
          || (width / inSampleSize) > mMaxTextureSize) {
        inSampleSize *= 2;
      }
    }
    return inSampleSize;
  }

  /**
   * Rotate the given bitmap by the given degrees.<br>
   * New bitmap is created and the old one is recycled.
   */
  private static Bitmap rotateAndFlipBitmapInt(
      Bitmap bitmap, int degrees, boolean flipHorizontally, boolean flipVertically) {
    if (degrees > 0 || flipHorizontally || flipVertically) {
      Matrix matrix = new Matrix();
      matrix.setRotate(degrees);
      matrix.postScale(flipHorizontally ? -1 : 1, flipVertically ? -1 : 1);
      Bitmap newBitmap =
          Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, false);
      if (newBitmap != bitmap) {
        bitmap.recycle();
      }
      return newBitmap;
    } else {
      return bitmap;
    }
  }

  /**
   * Get the max size of bitmap allowed to be rendered on the device.<br>
   * http://stackoverflow.com/questions/7428996/hw-accelerated-activity-how-to-get-opengl-texture-size-limit.
   */
  private static int getMaxTextureSize() {
    // Safe minimum default size
    final int IMAGE_MAX_BITMAP_DIMENSION = 2048;

    try {
      // Get EGL Display
      EGL10 egl = (EGL10) EGLContext.getEGL();
      EGLDisplay display = egl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);

      // Initialise
      int[] version = new int[2];
      egl.eglInitialize(display, version);

      // Query total number of configurations
      int[] totalConfigurations = new int[1];
      egl.eglGetConfigs(display, null, 0, totalConfigurations);

      // Query actual list configurations
      EGLConfig[] configurationsList = new EGLConfig[totalConfigurations[0]];
      egl.eglGetConfigs(display, configurationsList, totalConfigurations[0], totalConfigurations);

      int[] textureSize = new int[1];
      int maximumTextureSize = 0;

      // Iterate through all the configurations to located the maximum texture size
      for (int i = 0; i < totalConfigurations[0]; i++) {
        // Only need to check for width since opengl textures are always squared
        egl.eglGetConfigAttrib(
            display, configurationsList[i], EGL10.EGL_MAX_PBUFFER_WIDTH, textureSize);

        // Keep track of the maximum texture size
        if (maximumTextureSize < textureSize[0]) {
          maximumTextureSize = textureSize[0];
        }
      }

      // Release
      egl.eglTerminate(display);

      // Return largest texture size found, or default
      return Math.max(maximumTextureSize, IMAGE_MAX_BITMAP_DIMENSION);
    } catch (Exception e) {
      return IMAGE_MAX_BITMAP_DIMENSION;
    }
  }

  /**
   * Close the given closeable object (Stream) in a safe way: check if it is null and catch-log
   * exception thrown.
   *
   * @param closeable the closable object to close
   */
  private static void closeSafe(Closeable closeable) {
    if (closeable != null) {
      try {
        closeable.close();
      } catch (IOException ignored) {
      }
    }
  }
  // endregion

  // region: Inner class: BitmapSampled

  /** Holds bitmap instance and the sample size that the bitmap was loaded/cropped with. */
  static final class BitmapSampled {

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

    /** The sample size used to lower the size of the bitmap (1,2,4,8,...) */
    final int sampleSize;

    BitmapSampled(Bitmap bitmap, int sampleSize) {
      this.bitmap = bitmap;
      this.sampleSize = sampleSize;
    }
  }
  // endregion

  // region: Inner class: RotateBitmapResult

  /** The result of {@link #rotateBitmapByExif(android.graphics.Bitmap, ExifInterface)}. */
  static final class RotateBitmapResult {

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

    /** The degrees the image was rotated */
    final int degrees;

    RotateBitmapResult(Bitmap bitmap, int degrees) {
      this.bitmap = bitmap;
      this.degrees = degrees;
    }
  }
  // endregion
}
