package com.streamchatreactnative;

import android.content.Context;
import android.content.ContentResolver;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import androidx.exifinterface.media.ExifInterface;
import android.net.Uri;
import android.os.Build;
import android.provider.MediaStore;
import android.util.Base64;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Date;

/**
 * Provide methods to resize and rotate an image file.
 */
public class StreamChatReactNative {
  private final static String IMAGE_JPEG = "image/jpeg";
  private final static String IMAGE_PNG = "image/png";
  private final static String SCHEME_DATA = "data";
  private final static String SCHEME_CONTENT = "content";
  private final static String SCHEME_FILE = "file";
  private final static String SCHEME_HTTP = "http";
  private final static String SCHEME_HTTPS = "https";
  /**
   * Resize the specified bitmap.
   */
  private static Bitmap resizeImage(Bitmap image, int newWidth, int newHeight,
                                    String mode, boolean onlyScaleDown) {
    Bitmap newImage = null;
    if (image == null) {
      return null; // Can't load the image from the given path.
    }

    int width = image.getWidth();
    int height = image.getHeight();

    if (newHeight > 0 && newWidth > 0) {
      int finalWidth;
      int finalHeight;

      if (mode.equals("stretch")) {
        // Distort aspect ratio
        finalWidth = newWidth;
        finalHeight = newHeight;

        if (onlyScaleDown) {
          finalWidth = Math.min(width, finalWidth);
          finalHeight = Math.min(height, finalHeight);
        }
      } else {
        // "contain" (default) or "cover": keep its aspect ratio
        float widthRatio = (float) newWidth / width;
        float heightRatio = (float) newHeight / height;

        float ratio = mode.equals("cover") ?
          Math.max(widthRatio, heightRatio) :
          Math.min(widthRatio, heightRatio);

        if (onlyScaleDown) ratio = Math.min(ratio, 1);

        finalWidth = (int) Math.round(width * ratio);
        finalHeight = (int) Math.round(height * ratio);
      }

      try {
        newImage = Bitmap.createScaledBitmap(image, finalWidth, finalHeight, true);
      } catch (OutOfMemoryError e) {
        return null;
      }
    }

    return newImage;
  }

  /**
   * Rotate the specified bitmap with the given angle, in degrees.
   */
  public static Bitmap rotateImage(Bitmap source, Matrix matrix, float angle)
  {
    Bitmap retVal;
    matrix.postRotate(angle);

    try {
      retVal = Bitmap.createBitmap(source, 0, 0, source.getWidth(), source.getHeight(), matrix, true);
    } catch (OutOfMemoryError e) {
      return null;
    }
    return retVal;
  }

  /**
   * Save the given bitmap in a directory. Extension is automatically generated using the bitmap format.
   */
  public static File saveImage(Bitmap bitmap, File saveDirectory, String fileName,
                               Bitmap.CompressFormat compressFormat, int quality)
    throws IOException {
    if (bitmap == null) {
      throw new IOException("The bitmap couldn't be resized");
    }

    File newFile = new File(saveDirectory, fileName + "." + compressFormat.name());
    if(!newFile.createNewFile()) {
      throw new IOException("The file already exists");
    }

    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    bitmap.compress(compressFormat, quality, outputStream);
    byte[] bitmapData = outputStream.toByteArray();

    outputStream.flush();
    outputStream.close();

    FileOutputStream fos = new FileOutputStream(newFile);
    fos.write(bitmapData);
    fos.flush();
    fos.close();

    return newFile;
  }

  /**
   * Get {@link File} object for the given Android URI.<br>
   * Use content resolver to get real path if direct path doesn't return valid file.
   */
  private static File getFileFromUri(Context context, Uri uri) {

    // first try by direct path
    File file = new File(uri.getPath());
    if (file.exists()) {
      return file;
    }

    // try reading real path from content resolver (gallery images)
    Cursor cursor = null;
    try {
      String[] proj = {MediaStore.Images.Media.DATA};
      cursor = context.getContentResolver().query(uri, proj, null, null, null);
      int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
      cursor.moveToFirst();
      String realPath = cursor.getString(column_index);
      file = new File(realPath);
    } catch (Exception ignored) {
    } finally {
      if (cursor != null) {
        cursor.close();
      }
    }

    return file;
  }

  /**
   * Get orientation by reading Image metadata
   */
  public static Matrix getOrientationMatrix(Context context, Uri uri) {
    try {
      // ExifInterface(InputStream) only exists since Android N (r24)
      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
        InputStream input = context.getContentResolver().openInputStream(uri);
        ExifInterface ei = new ExifInterface(input);
        return getOrientationMatrix(ei);
      }
      File file = getFileFromUri(context, uri);
      if (file.exists()) {
        ExifInterface ei = new ExifInterface(file.getAbsolutePath());
        return getOrientationMatrix(ei);
      }
    } catch (Exception ignored) { }

    return new Matrix();
  }

  /**
   * Convert metadata to degrees
   */
  public static Matrix getOrientationMatrix(ExifInterface exif) {
    Matrix matrix = new Matrix();
    int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED);
    switch (orientation) {
      case ExifInterface.ORIENTATION_FLIP_HORIZONTAL:
        matrix.setScale(-1, 1);
        break;
      case ExifInterface.ORIENTATION_TRANSPOSE:
        matrix.setRotate(90);
        matrix.postScale(-1, 1);
        break;
      case ExifInterface.ORIENTATION_ROTATE_90:
        matrix.setRotate(90);
        break;
      case ExifInterface.ORIENTATION_FLIP_VERTICAL:
        matrix.setRotate(180);
        matrix.postScale(-1, 1);
        break;
      case ExifInterface.ORIENTATION_ROTATE_180:
        matrix.setRotate(180);
        break;
      case ExifInterface.ORIENTATION_TRANSVERSE:
        matrix.setRotate(270);
        matrix.postScale(-1, 1);
        break;
      case ExifInterface.ORIENTATION_ROTATE_270:
        matrix.setRotate(270);
        break;
      }
    return matrix;
  }

  /**
   * Compute the inSampleSize value to use to load a bitmap.
   * Adapted from https://developer.android.com/training/displaying-bitmaps/load-bitmap.html
   */
  private static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
    final int height = options.outHeight;
    final int width = options.outWidth;

    int inSampleSize = 1;

    if (height > reqHeight || width > reqWidth) {
      final int halfHeight = height / 2;
      final int halfWidth = width / 2;

      // Calculate the largest inSampleSize value that is a power of 2 and keeps both
      // height and width larger than the requested height and width.
      while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) {
        inSampleSize *= 2;
      }
    }

    return inSampleSize;
  }

  /**
   * Load a bitmap either from a real file or using the {@link ContentResolver} of the current
   * {@link Context} (to read gallery images for example).
   *
   * Note that, when options.inJustDecodeBounds = true, we actually expect sourceImage to remain
   * as null (see https://developer.android.com/training/displaying-bitmaps/load-bitmap.html), so
   * getting null sourceImage at the completion of this method is not always worthy of an error.
   */
  private static Bitmap loadBitmap(Context context, Uri imageUri, BitmapFactory.Options options) throws IOException {
    Bitmap sourceImage = null;
    String imageUriScheme = imageUri.getScheme();
    if (imageUriScheme == null || !imageUriScheme.equalsIgnoreCase(SCHEME_CONTENT)) {
      try {
        sourceImage = BitmapFactory.decodeFile(imageUri.getPath(), options);
      } catch (Exception e) {
        e.printStackTrace();
        throw new IOException("Error decoding image file");
      }
    } else {
      ContentResolver cr = context.getContentResolver();
      InputStream input = cr.openInputStream(imageUri);
      if (input != null) {
        sourceImage = BitmapFactory.decodeStream(input, null, options);
        input.close();
      }
    }
    return sourceImage;
  }

  /**
   * Loads the bitmap resource from the file specified in imagePath.
   */
  private static Bitmap loadBitmapFromFile(Context context, Uri imageUri, int newWidth,
                                           int newHeight) throws IOException  {
    // Decode the image bounds to find the size of the source image.
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    loadBitmap(context, imageUri, options);

    // Set a sample size according to the image size to lower memory usage.
    options.inSampleSize = calculateInSampleSize(options, newWidth, newHeight);
    options.inJustDecodeBounds = false;
    //System.out.println(options.inSampleSize);
    return loadBitmap(context, imageUri, options);

  }

  /**
   * Loads the bitmap resource from an URL
   */
  private static Bitmap loadBitmapFromURL(Uri imageUri, int newWidth,
                                          int newHeight) throws IOException  {

    InputStream input = null;
    Bitmap sourceImage = null;

    try{
      URL url = new URL(imageUri.toString());
      HttpURLConnection connection = (HttpURLConnection) url.openConnection();
      connection.connect();
      input = connection.getInputStream();

      if (input != null) {

        // need to load into memory since inputstream is not seekable
        // we still won't load the whole bitmap into memory
        // Also need this ugly code since we are on Java8...
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        int nRead;
        byte[] data = new byte[1024];
        byte[] imageData = null;

        try{
          while ((nRead = input.read(data, 0, data.length)) != -1) {
            buffer.write(data, 0, nRead);
          }
          buffer.flush();
          imageData = buffer.toByteArray();
        }
        finally{
          buffer.close();
        }


        // Decode the image bounds to find the size of the source image.
        // Do it here so we only do one request
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeByteArray(imageData, 0, imageData.length, options);

        // Set a sample size according to the image size to lower memory usage.
        options.inSampleSize = calculateInSampleSize(options, newWidth, newHeight);
        options.inJustDecodeBounds = false;

        sourceImage = BitmapFactory.decodeByteArray(imageData, 0, imageData.length, options);
      }
    }
    catch (Exception e) {
      e.printStackTrace();
      throw new IOException("Error fetching remote image file.");
    }
    finally{
      try {
        if(input != null){
          input.close();
        }
      }
      catch (IOException e) {
        e.printStackTrace();
      }

    }

    return sourceImage;

  }

  /**
   * Loads the bitmap resource from a base64 encoded jpg or png.
   * Format is as such:
   * png: 'data:image/png;base64,iVBORw0KGgoAA...'
   * jpg: 'data:image/jpeg;base64,/9j/4AAQSkZJ...'
   */
  private static Bitmap loadBitmapFromBase64(Uri imageUri) {
    Bitmap sourceImage = null;
    String imagePath = imageUri.getSchemeSpecificPart();
    int commaLocation = imagePath.indexOf(',');
    if (commaLocation != -1) {
      final String mimeType = imagePath.substring(0, commaLocation).replace('\\','/').toLowerCase();
      final boolean isJpeg = mimeType.startsWith(IMAGE_JPEG);
      final boolean isPng = !isJpeg && mimeType.startsWith(IMAGE_PNG);

      if (isJpeg || isPng) {
        // base64 image. Convert to a bitmap.
        final String encodedImage = imagePath.substring(commaLocation + 1);
        final byte[] decodedString = Base64.decode(encodedImage, Base64.DEFAULT);
        sourceImage = BitmapFactory.decodeByteArray(decodedString, 0, decodedString.length);
      }
    }

    return sourceImage;
  }

  /**
   * Create a resized version of the given image and returns a Bitmap object
   * ready to be saved or converted. Ensure that the result is cleaned up after use
   * by using recycle
   */
  public static Bitmap createResizedImage(Context context, Uri imageUri, int newWidth,
                                          int newHeight, int quality, int rotation,
                                          String mode, boolean onlyScaleDown) throws IOException  {
    Bitmap sourceImage = null;
    String imageUriScheme = imageUri.getScheme();

    if (imageUriScheme == null ||
      imageUriScheme.equalsIgnoreCase(SCHEME_FILE) ||
      imageUriScheme.equalsIgnoreCase(SCHEME_CONTENT)
    ) {
      sourceImage = StreamChatReactNative.loadBitmapFromFile(context, imageUri, newWidth, newHeight);
    } else if (imageUriScheme.equalsIgnoreCase(SCHEME_HTTP) || imageUriScheme.equalsIgnoreCase(SCHEME_HTTPS)){
      sourceImage = StreamChatReactNative.loadBitmapFromURL(imageUri, newWidth, newHeight);
    } else if (imageUriScheme.equalsIgnoreCase(SCHEME_DATA)) {
      sourceImage = StreamChatReactNative.loadBitmapFromBase64(imageUri);
    }

    if (sourceImage == null) {
      throw new IOException("Unable to load source image from path");
    }


    // Rotate if necessary. Rotate first because we will otherwise
    // get wrong dimensions if we want the new dimensions to be after rotation.
    // NOTE: This will "fix" the image using it's exif info if it is rotated as well.
    Bitmap rotatedImage = sourceImage;
    Matrix matrix = getOrientationMatrix(context, imageUri);
    rotatedImage = StreamChatReactNative.rotateImage(sourceImage, matrix, rotation);

    if(rotatedImage == null){
      throw new IOException("Unable to rotate image. Most likely due to not enough memory.");
    }

    if (rotatedImage != sourceImage) {
      sourceImage.recycle();
    }

    // Scale image
    Bitmap scaledImage = StreamChatReactNative.resizeImage(rotatedImage, newWidth, newHeight, mode, onlyScaleDown);

    if(scaledImage == null){
      throw new IOException("Unable to resize image. Most likely due to not enough memory.");
    }

    if (scaledImage != rotatedImage) {
      rotatedImage.recycle();
    }

    return scaledImage;
  }
}
