package com.lemenzo.gallery.loader;

import android.content.ContentResolver;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.ImageDecoder;
import android.graphics.Matrix;
import android.media.ExifInterface;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.util.Size;

import java.io.IOException;
import java.io.InputStream;
import java.util.Locale;

/** Defensive full-screen image decoding for MediaStore/content URIs. */
public final class BitmapDecodeUtils {
    private BitmapDecodeUtils() {}

    public static Bitmap decode(ContentResolver resolver, Uri uri, int maxWidth, int maxHeight)
            throws IOException {
        String mime = null;
        try { mime = resolver.getType(uri); } catch (RuntimeException ignored) {}
        String normalized = mime == null ? "" : mime.toLowerCase(Locale.ROOT);

        // JPEG/PNG/BMP are extremely common download/WhatsApp formats. BitmapFactory is
        // deliberately primary for these because it is independent from ImageDecoder and
        // avoids device-specific ImageDecoder rendering anomalies that can return a valid
        // but unusable full-screen bitmap without throwing.
        boolean commonStill = normalized.equals("image/jpeg")
                || normalized.equals("image/jpg")
                || normalized.equals("image/png")
                || normalized.equals("image/bmp")
                || normalized.equals("image/x-ms-bmp");

        Throwable firstFailure = null;
        if (commonStill) {
            try {
                Bitmap bitmap = decodeWithBitmapFactory(resolver, uri, maxWidth, maxHeight);
                if (bitmap != null) return bitmap;
            } catch (IOException | RuntimeException | OutOfMemoryError failure) {
                firstFailure = failure;
            }
            try {
                return decodeWithImageDecoder(resolver, uri, maxWidth, maxHeight);
            } catch (IOException | RuntimeException | OutOfMemoryError failure) {
                IOException combined = new IOException("Unable to decode image " + uri, failure);
                if (firstFailure != null) combined.addSuppressed(firstFailure);
                throw combined;
            }
        }

        // Modern formats (HEIF/HEIC, AVIF, WebP and vendor-supported formats) use
        // ImageDecoder first, then a genuinely independent BitmapFactory fallback.
        try {
            return decodeWithImageDecoder(resolver, uri, maxWidth, maxHeight);
        } catch (IOException | RuntimeException | OutOfMemoryError failure) {
            firstFailure = failure;
        }
        try {
            Bitmap fallback = decodeWithBitmapFactory(resolver, uri, maxWidth, maxHeight);
            if (fallback != null) return fallback;
            throw new IOException("BitmapFactory returned no bitmap for " + uri);
        } catch (IOException | RuntimeException | OutOfMemoryError fallbackFailure) {
            IOException combined = new IOException("Unable to decode image " + uri, fallbackFailure);
            if (firstFailure != null) combined.addSuppressed(firstFailure);
            throw combined;
        }
    }

    private static Bitmap decodeWithImageDecoder(
            ContentResolver resolver, Uri uri, int maxWidth, int maxHeight) throws IOException {
        ImageDecoder.Source source = ImageDecoder.createSource(resolver, uri);
        return ImageDecoder.decodeBitmap(source, (decoder, info, src) -> {
            Size size = info.getSize();
            int width = Math.max(1, size.getWidth());
            int height = Math.max(1, size.getHeight());
            int[] target = targetSize(width, height, maxWidth, maxHeight);
            if (target[0] != width || target[1] != height) {
                decoder.setTargetSize(target[0], target[1]);
            }
            decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
            decoder.setOnPartialImageListener(exception -> true);
        });
    }

    private static Bitmap decodeWithBitmapFactory(
            ContentResolver resolver, Uri uri, int maxWidth, int maxHeight) throws IOException {
        BitmapFactory.Options bounds = new BitmapFactory.Options();
        bounds.inJustDecodeBounds = true;
        decodeBounds(resolver, uri, bounds);
        if (bounds.outWidth <= 0 || bounds.outHeight <= 0) {
            throw new IOException("Invalid image dimensions for " + uri);
        }

        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inPreferredConfig = Bitmap.Config.ARGB_8888;
        options.inDither = false;
        options.inSampleSize = calculateInSampleSize(
                bounds.outWidth, bounds.outHeight,
                Math.max(1, maxWidth), Math.max(1, maxHeight));

        Bitmap bitmap = decodeBitmap(resolver, uri, options);
        if (bitmap == null) return null;

        bitmap = applyExifOrientation(resolver, uri, bitmap);

        int[] target = targetSize(
                bitmap.getWidth(), bitmap.getHeight(), maxWidth, maxHeight);
        if (target[0] != bitmap.getWidth() || target[1] != bitmap.getHeight()) {
            Bitmap scaled = Bitmap.createScaledBitmap(bitmap, target[0], target[1], true);
            if (scaled != bitmap) bitmap.recycle();
            bitmap = scaled;
        }
        return bitmap;
    }

    private static Bitmap applyExifOrientation(ContentResolver resolver, Uri uri, Bitmap bitmap) {
        int orientation = ExifInterface.ORIENTATION_NORMAL;
        try (InputStream input = resolver.openInputStream(uri)) {
            if (input != null) {
                ExifInterface exif = new ExifInterface(input);
                orientation = exif.getAttributeInt(
                        ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
            }
        } catch (IOException | RuntimeException ignored) {
            return bitmap;
        }

        Matrix matrix = new Matrix();
        switch (orientation) {
            case ExifInterface.ORIENTATION_FLIP_HORIZONTAL:
                matrix.setScale(-1f, 1f); break;
            case ExifInterface.ORIENTATION_ROTATE_180:
                matrix.setRotate(180f); break;
            case ExifInterface.ORIENTATION_FLIP_VERTICAL:
                matrix.setScale(1f, -1f); break;
            case ExifInterface.ORIENTATION_TRANSPOSE:
                matrix.setRotate(90f); matrix.postScale(-1f, 1f); break;
            case ExifInterface.ORIENTATION_ROTATE_90:
                matrix.setRotate(90f); break;
            case ExifInterface.ORIENTATION_TRANSVERSE:
                matrix.setRotate(-90f); matrix.postScale(-1f, 1f); break;
            case ExifInterface.ORIENTATION_ROTATE_270:
                matrix.setRotate(-90f); break;
            default:
                return bitmap;
        }
        try {
            Bitmap corrected = Bitmap.createBitmap(
                    bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
            if (corrected != bitmap) bitmap.recycle();
            return corrected;
        } catch (RuntimeException | OutOfMemoryError ignored) {
            return bitmap;
        }
    }

    private static void decodeBounds(ContentResolver resolver, Uri uri, BitmapFactory.Options options)
            throws IOException {
        try (ParcelFileDescriptor descriptor = resolver.openFileDescriptor(uri, "r")) {
            if (descriptor != null) {
                BitmapFactory.decodeFileDescriptor(descriptor.getFileDescriptor(), null, options);
                if (options.outWidth > 0 && options.outHeight > 0) return;
            }
        } catch (RuntimeException ignored) {}
        try (InputStream input = resolver.openInputStream(uri)) {
            if (input == null) throw new IOException("Unable to open image stream " + uri);
            BitmapFactory.decodeStream(input, null, options);
        }
    }

    private static Bitmap decodeBitmap(ContentResolver resolver, Uri uri, BitmapFactory.Options options)
            throws IOException {
        try (ParcelFileDescriptor descriptor = resolver.openFileDescriptor(uri, "r")) {
            if (descriptor != null) {
                Bitmap result = BitmapFactory.decodeFileDescriptor(
                        descriptor.getFileDescriptor(), null, options);
                if (result != null) return result;
            }
        } catch (RuntimeException ignored) {}
        try (InputStream input = resolver.openInputStream(uri)) {
            if (input == null) throw new IOException("Unable to open image stream " + uri);
            return BitmapFactory.decodeStream(input, null, options);
        }
    }

    private static int calculateInSampleSize(
            int width, int height, int maxWidth, int maxHeight) {
        int sample = 1;
        while (width / (sample * 2) >= maxWidth
                && height / (sample * 2) >= maxHeight) {
            sample *= 2;
        }
        return Math.max(1, sample);
    }

    private static int[] targetSize(int width, int height, int maxWidth, int maxHeight) {
        int safeMaxWidth = Math.max(1, maxWidth);
        int safeMaxHeight = Math.max(1, maxHeight);
        float scale = Math.min(1f, Math.min(
                (float) safeMaxWidth / Math.max(1, width),
                (float) safeMaxHeight / Math.max(1, height)));
        return new int[] {
                Math.max(1, Math.round(width * scale)),
                Math.max(1, Math.round(height * scale))
        };
    }
}
