/*  Copyright (C) 2017-2024 Arjan Schrijver, Frank Slezak, HelloCodeberg,
    José Rebelo

    This file is part of Gadgetbridge.

    Gadgetbridge is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published
    by the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    Gadgetbridge is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>. */

package nodomain.freeyourgadget.gadgetbridge.util;

import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.ColorMatrix;
import android.graphics.ColorMatrixColorFilter;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;

import androidx.annotation.Nullable;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;

public class BitmapUtil {
    private static final Logger LOG = LoggerFactory.getLogger(BitmapUtil.class);

    /**
     * Downscale a bitmap to a maximum resolution. Doesn't scale if the bitmap is already smaller than the max resolution.
     *
     * @param bitmap
     * @param maxWidth
     * @param maxHeight
     * @return
     */
    public static Bitmap scaleWithMax(Bitmap bitmap, int maxWidth, int maxHeight) {
        // Scale image only if necessary
        if ((bitmap.getWidth() > maxWidth) || (bitmap.getHeight() > maxHeight)) {
            bitmap = Bitmap.createScaledBitmap(bitmap, maxWidth, maxHeight, true);
        }
        return bitmap;
    }

    /**
     * Get a Bitmap from any given Drawable.
     *
     * Note that this code will fail if the drawable is 0x0.
     *
     * @param drawable A Drawable to convert.
     * @return A Bitmap representing the drawable.
     */
    public static Bitmap convertDrawableToBitmap(Drawable drawable) {
        // If whoever made this drawable decided to be nice to us...
        if (drawable instanceof BitmapDrawable) {
            return ((BitmapDrawable) drawable).getBitmap();
        }

        Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);

        drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
        drawable.draw(canvas);

        return bitmap;
    }

    /**
     * Converts the provided Bitmap to grayscale.
     *
     * @param bitmap
     * @return
     */
    public static Bitmap convertToGrayscale(Bitmap bitmap) {
        Canvas c = new Canvas(bitmap);
        Paint paint = new Paint();
        ColorMatrix cm = new ColorMatrix();
        cm.setSaturation(0);
        ColorMatrixColorFilter f = new ColorMatrixColorFilter(cm);
        paint.setColorFilter(f);
        c.drawBitmap(bitmap, 0, 0, paint);
        return bitmap;
    }

    /**
     * Change the contrast and brightness on a Bitmap
     *
     * Code from: https://stackoverflow.com/questions/12891520/how-to-programmatically-change-contrast-of-a-bitmap-in-android#17887577
     *
     * @param bmp input bitmap
     * @param contrast 0..10 1 is default
     * @param brightness -255..255 0 is default
     * @return new bitmap
     */
    public static Bitmap changeBitmapContrastBrightness(Bitmap bmp, float contrast, float brightness)
    {
        ColorMatrix cm = new ColorMatrix(new float[]
                {
                        contrast, 0, 0, 0, brightness,
                        0, contrast, 0, 0, brightness,
                        0, 0, contrast, 0, brightness,
                        0, 0, 0, 1, 0
                });

        Bitmap ret = Bitmap.createBitmap(bmp.getWidth(), bmp.getHeight(), bmp.getConfig());

        Canvas canvas = new Canvas(ret);

        Paint paint = new Paint();
        paint.setColorFilter(new ColorMatrixColorFilter(cm));
        canvas.drawBitmap(bmp, 0, 0, paint);

        return ret;
    }


    /**
     * Invert the colors of a Bitmap
     *
     * @param bmp input bitmap
     * @return new bitmap
     */
    public static Bitmap invertBitmapColors(Bitmap bmp)
    {
        ColorMatrix colorMatrix_Inverted =
                new ColorMatrix(new float[] {
                        -1,  0,  0,  0, 255,
                        0, -1,  0,  0, 255,
                        0,  0, -1,  0, 255,
                        0,  0,  0,  1,   0});

        Bitmap ret = Bitmap.createBitmap(bmp.getWidth(), bmp.getHeight(), bmp.getConfig());

        Canvas canvas = new Canvas(ret);

        Paint paint = new Paint();
        paint.setColorFilter(new ColorMatrixColorFilter(colorMatrix_Inverted));
        canvas.drawBitmap(bmp, 0, 0, paint);

        return ret;
    }


    /**
     * Crops a circular image from the center of the provided Bitmap.
     * From: https://www.tutorialspoint.com/android-how-to-crop-circular-area-from-bitmap
     * @param srcBitmap
     * @return
     */
    public static Bitmap getCircularBitmap(Bitmap srcBitmap) {
        // Calculate the circular bitmap width with border
        int squareBitmapWidth = Math.min(srcBitmap.getWidth(), srcBitmap.getHeight());
        // Initialize a new instance of Bitmap
        Bitmap dstBitmap = Bitmap.createBitmap (
                squareBitmapWidth, // Width
                squareBitmapWidth, // Height
                Bitmap.Config.ARGB_8888 // Config
        );
        Canvas canvas = new Canvas(dstBitmap);
        // Initialize a new Paint instance
        Paint paint = new Paint();
        paint.setAntiAlias(true);
        Rect rect = new Rect(0, 0, squareBitmapWidth, squareBitmapWidth);
        RectF rectF = new RectF(rect);
        canvas.drawOval(rectF, paint);
        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
        // Calculate the left and top of copied bitmap
        float left = (squareBitmapWidth-srcBitmap.getWidth())/2;
        float top = (squareBitmapWidth-srcBitmap.getHeight())/2;
        canvas.drawBitmap(srcBitmap, left, top, paint);
        // Return the circular bitmap
        return dstBitmap;
    }

    /**
     * Rotates a given Bitmap
     * @param bitmap input bitmap
     * @param degree int Degree of rotation
     * @return new bitmap
     */
    public static Bitmap rotateImage(Bitmap bitmap, int degree) {
        Matrix matrix = new Matrix();
        matrix.postRotate(degree);
        return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
    }


    /**
     * Overlays two bitmaps on top of each other,
     * bmp1 is assumed to be larger or equal to bmp2
     * From: https://stackoverflow.com/a/2287218
     * @param bmp1
     * @param bmp2
     * @return new Bitmap
     */
    public static Bitmap overlay(Bitmap bmp1, Bitmap bmp2) {
        Bitmap bmOverlay = Bitmap.createBitmap(bmp1.getWidth(), bmp1.getHeight(), bmp1.getConfig());
        Canvas canvas = new Canvas(bmOverlay);
        canvas.drawBitmap(bmp1, new Matrix(), null);
        canvas.drawBitmap(bmp2, new Matrix(), null);
        return bmOverlay;
    }

    /**
     * Converts a {@link Drawable} to a {@link Bitmap}, in ARGB8888 mode.
     *
     * @param drawable the {@link Drawable}
     * @return the {@link Bitmap}
     */
    public static Bitmap toBitmap(final Drawable drawable) {
        if (drawable instanceof BitmapDrawable) {
            return ((BitmapDrawable) drawable).getBitmap();
        }

        final Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
        final Canvas canvas = new Canvas(bitmap);
        drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
        drawable.draw(canvas);
        return bitmap;
    }

    /**
     * Converts a {@link Bitmap} to an uncompressed TGA image, as raw bytes, in RGB565 encoding.
     * @param bmp the {@link Bitmap} to convert.
     * @param width the target width
     * @param height the target height
     * @param id the TGA ID
     * @return the raw bytes for the TGA image
     */
    public static byte[] convertToTgaRGB565(final Bitmap bmp, final int width, final int height, final byte[] id) {
        final Bitmap bmp565;
        if (bmp.getConfig().equals(Bitmap.Config.RGB_565) && bmp.getWidth() == width && bmp.getHeight() == height) {
            // Right encoding and size
            bmp565 = bmp;
        } else {
            // Convert encoding / scale
            bmp565 = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565);
            final Canvas canvas = new Canvas(bmp565);
            final Rect rect = new Rect(0, 0, width, height);
            canvas.drawBitmap(bmp, null, rect, null);
        }

        int size = bmp565.getRowBytes() * bmp565.getHeight();
        final ByteBuffer bmp565buf = ByteBuffer.allocate(size);
        bmp565.copyPixelsToBuffer(bmp565buf);

        // As per https://en.wikipedia.org/wiki/Truevision_TGA
        // 18 bytes
        final byte[] header = {
                // ID length
                (byte) id.length,
                // Color map type - (0 - no color map)
                0x00,
                // Image type (2 - uncompressed true-color image)
                0x02,
                // Color map specification (5 bytes)
                0x00, 0x00, // first entry index
                0x00, 0x00, /// color map length
                0x00, // color map entry size
                // Image dimensions and format (10 bytes)
                0x00, 0x00, // x origin
                0x00, 0x00, // y origin
                (byte) (width & 0xff), (byte) ((width >> 8) & 0xff), // width
                (byte) (height & 0xff), (byte) ((height >> 8) & 0xff), // height
                16, // bits per pixel (10)
                0x20, // image descriptor (0x20, 00100000)
                // bits 3-0 give the alpha channel depth, bits 5-4 give pixel ordering
                // Bit 4 of the image descriptor byte indicates right-to-left pixel ordering if set.
                // Bit 5 indicates an ordering of top-to-bottom. Otherwise, pixels are stored in bottom-to-top, left-to-right order.
        };

        final ByteBuffer tga565buf = ByteBuffer.allocate(header.length + id.length + size);

        tga565buf.put(header);
        tga565buf.put(id);
        tga565buf.put(bmp565buf.array());

        return tga565buf.array();
    }

    public static boolean isPng(final byte[] data) {
        return ArrayUtils.equals(data, new byte[] {(byte) 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a}, 0);
    }

    @Nullable
    public static Bitmap decodeTga(final byte[] bytes) {
        final ByteBuffer buf = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN);
        final byte idLength = buf.get();
        final byte colorMapType = buf.get();
        final byte imageType = buf.get();
        final short colorMapFirstEntryIndex = buf.getShort();
        final short colorMapLength = buf.getShort();
        final byte colorMapEntrySize = buf.get();
        final short xOrigin = buf.getShort();
        final short yOrigin = buf.getShort();
        final short width = buf.getShort();
        final short height = buf.getShort();
        final byte bitsPerPixel = buf.get();
        final byte imageDescriptor = buf.get();

        final byte[] id = new byte[idLength];
        buf.get(id);

        if (imageType== 2 && colorMapType == 0) {
            // Parse true-color uncompressed image data as RGB565
            final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565);
            bitmap.copyPixelsFromBuffer(buf);

            return bitmap;
        }

        if (colorMapType != 0x01) {
            LOG.warn("Unknown color map type {}", colorMapType);
            return null;
        }

        if (colorMapEntrySize != 32) {
            LOG.warn("Color map entry size {} not supported", colorMapEntrySize);
            return null;
        }

        final int[] colorMap = new int[colorMapLength];
        for (int i = 0; i < colorMapLength; i++) {
            colorMap[i] = buf.getInt();
        }

        final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);

        switch (imageType) {
            case 1:
                if (bitsPerPixel != 8) {
                    LOG.warn("Unsupported bits per pixel {} for imageType 1", bitsPerPixel);
                    return null;
                }

                // uncompressed color-mapped image
                for (int y = 0; y < height; y++) {
                    for (int x = 0; x < width; x++) {
                        bitmap.setPixel(x, y, colorMap[buf.get() & 0xff]);
                    }
                }
                break;
            case 9:
                // run-length encoded color-mapped image
                int i = 0;
                while (buf.position() < buf.limit()) {
                    final byte b = buf.get();
                    final int count = (b & 127) + 1;
                    if ((b & 128) != 0) {
                        // msb 1 - run-length encoded
                        final int val = buf.get() & 0xff;
                        for (int j = 0; j < count; j++) {
                            int y = i / width;
                            int x = i % width;
                            bitmap.setPixel(x, y, colorMap[val]);
                            i++;
                        }
                    } else {
                        // msb 0 - raw pixels
                        for (int j = 0; j < count; j++) {
                            int y = i / width;
                            int x = i % width;
                            bitmap.setPixel(x, y, colorMap[buf.get() & 0xff]);
                            i++;
                        }
                    }
                }
                break;
            default:
                LOG.warn("Image type {} not supported", imageType);
                return null;
        }

        final int remainingBytes = buf.limit() - buf.position();
        if (remainingBytes != 0) {
            LOG.warn("There are {} bytes remaining in the buffer", remainingBytes);
        }

        return bitmap;
    }
}
