/*
 * CameraUtil.java
 * Copyright (c) 2018
 * Authors: Ionut Damian, Michael Dietz, Frank Gaibler, Daniel Langerenken, Simon Flutura,
 * Vitalijs Krumins, Antonio Grieco
 * *****************************************************
 * This file is part of the Social Signal Interpretation for Java (SSJ) framework
 * developed at the Lab for Human Centered Multimedia of the University of Augsburg.
 *
 * SSJ has been inspired by the SSI (http://openssi.net) framework. SSJ is not a
 * one-to-one port of SSI to Java, it is an approximation. Nor does SSJ pretend
 * to offer SSI's comprehensive functionality and performance (this is java after all).
 * Nevertheless, SSJ borrows a lot of programming patterns from SSI.
 *
 * This library is free software; you can redistribute it and/or modify it under the
 * terms of the GNU General Public License as published by the Free Software
 * Foundation; either version 3 of the License, or any later version.
 *
 * This library 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this library; if not, see <http://www.gnu.org/licenses/>.
 */

package hcm.ssj.camera;

import android.graphics.Bitmap;
import android.graphics.Matrix;
import android.media.MediaCodecInfo;
import android.media.MediaCodecList;

import java.io.File;
import java.io.FileOutputStream;
import java.util.Date;

import hcm.ssj.core.Log;
import hcm.ssj.file.FileCons;

/**
 * Utility class for the camera. <br>
 * Created by Frank Gaibler on 26.01.2016.
 */
@SuppressWarnings("deprecation")
public class CameraUtil
{
    /**
     * Returns the first codec capable of encoding the specified MIME type, or null if no
     * match was found.
     *
     * @param mimeType String
     * @return MediaCodecInfo
     */
    public static MediaCodecInfo selectCodec(String mimeType)
    {
        int numCodecs = MediaCodecList.getCodecCount();
        for (int i = 0; i < numCodecs; i++)
        {
            MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i);
            if (!codecInfo.isEncoder())
            {
                continue;
            }
            String[] types = codecInfo.getSupportedTypes();
            for (String type : types)
            {
                if (type.equalsIgnoreCase(mimeType))
                {
                    return codecInfo;
                }
            }
        }
        return null;
    }

    /**
     * Returns a color format that is supported by the codec and by this code.  If no
     * match is found, an exception is thrown.
     *
     * @param codecInfo MediaCodecInfo
     * @param mimeType  String
     * @return int
     */
    public static int selectColorFormat(MediaCodecInfo codecInfo, String mimeType)
    {
        MediaCodecInfo.CodecCapabilities capabilities = codecInfo.getCapabilitiesForType(mimeType);
        for (int i = 0; i < capabilities.colorFormats.length; i++)
        {
            int colorFormat = capabilities.colorFormats[i];
            if (isRecognizedFormat(colorFormat))
            {
                return colorFormat;
            }
        }
        Log.e("couldn't find a good color format for " + codecInfo.getName() + " / " + mimeType);
        return 0;   // not reached
    }

    /**
     * Returns true if this is a common color format.
     *
     * @param colorFormat int
     * @return boolean
     */
    private static boolean isRecognizedFormat(int colorFormat)
    {
        switch (colorFormat)
        {
            case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar:
            case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedPlanar:
            case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar:
            case MediaCodecInfo.CodecCapabilities.COLOR_FormatYCrYCb:
            case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedSemiPlanar:
            case MediaCodecInfo.CodecCapabilities.COLOR_TI_FormatYUV420PackedSemiPlanar:
                return true;
            default:
                return false;
        }
    }

    /**
     * Decodes YUVNV21 color space into a regular RGB format.
     *
     * @param rgb Output array for RGB values.
     * @param yuv YUV byte data to decode.
     * @param width Width of image in pixels.
     * @param height Height of image in pixels.
     */
    public static void convertNV21ToRGB_slow(byte[] rgb, byte[] yuv, int width, int height) {
        final int frameSize = width * height;
        final int ii = 0;
        final int ij = 0;
        final int di = +1;
        final int dj = +1;

        int a = 0;
        for (int i = 0, ci = ii; i < height; ++i, ci += di) {
            for (int j = 0, cj = ij; j < width; ++j, cj += dj) {
                int y = (0xff & ((int) yuv[ci * width + cj]));
                int v = (0xff & ((int) yuv[frameSize + (ci >> 1) * width + (cj & ~1) + 0]));
                int u = (0xff & ((int) yuv[frameSize + (ci >> 1) * width + (cj & ~1) + 1]));
                y = y < 16 ? 16 : y;

                int r = (int) (1.164f * (y - 16) + 1.596f * (v - 128));
                int g = (int) (1.164f * (y - 16) - 0.813f * (v - 128) - 0.391f * (u - 128));
                int b = (int) (1.164f * (y - 16) + 2.018f * (u - 128));

                rgb[a++] = (byte)(r < 0 ? 0 : (r > 255 ? 255 : r)); // red
                rgb[a++] = (byte)(g < 0 ? 0 : (g > 255 ? 255 : g)); // green
                rgb[a++] = (byte)(b < 0 ? 0 : (b > 255 ? 255 : b)); // blue
            }
        }
    }

    public static void convertNV21ToARGBInt_slow(int[] argb, byte[] yuv, int width, int height)
    {
        final int frameSize = width * height;
        final int ii = 0;
        final int ij = 0;
        final int di = +1;
        final int dj = +1;

        int a = 0;
        for (int i = 0, ci = ii; i < height; ++i, ci += di) {
            for (int j = 0, cj = ij; j < width; ++j, cj += dj) {
                int y = (0xff & ((int) yuv[ci * width + cj]));
                int v = (0xff & ((int) yuv[frameSize + (ci >> 1) * width + (cj & ~1) + 0]));
                int u = (0xff & ((int) yuv[frameSize + (ci >> 1) * width + (cj & ~1) + 1]));
                y = y < 16 ? 16 : y;

                int r = (int) (1.164f * (y - 16) + 1.596f * (v - 128));
                int g = (int) (1.164f * (y - 16) - 0.813f * (v - 128) - 0.391f * (u - 128));
                int b = (int) (1.164f * (y - 16) + 2.018f * (u - 128));

                 r = r < 0 ? 0 : (r > 255 ? 255 : r);
                 g = g < 0 ? 0 : (g > 255 ? 255 : g);
                 b = b < 0 ? 0 : (b > 255 ? 255 : b);

                 argb[a++] = 0xff000000 | (r << 16) | (g << 8) | b;
            }
        }
    }

    /**
     * Decodes YUVNV21 color space into a regular RGB format.
     *
     * @param rgb Output array for RGB values.
     * @param yuv YUV byte data to decode.
     * @param width Width of image in pixels.
     * @param height Height of image in pixels.
     */
    public static void convertNV21ToRGB(byte[] rgb, byte[] yuv, int width, int height)
    {
        convertNV21ToRGB(rgb, yuv, width, height, true);
    }

    /**
     * Decodes YUVNV21 color space into a regular RGB format.
     *
     * @param out Output array for RGB values.
     * @param yuv YUV byte data to decode.
     * @param width Width of image in pixels.
     * @param height Height of image in pixels.
     * @param swap swap U with V (default = true)
     */
    public static void convertNV21ToRGB(byte[] out, byte[] yuv, int width, int height, boolean swap)
    {
        int sz = width * height;
        int i, j;
        int Y, Cr = 0, Cb = 0;
        int outPtr = 0;
        for (j = 0; j < height; j++)
        {
            int pixPtr = j * width;
            final int jDiv2 = j >> 1;
            for (i = 0; i < width; i++)
            {
                Y = yuv[pixPtr];
                if (Y < 0)
                    Y += 255;
                if ((i & 0x1) != 1)
                {
                    final int cOff = sz + jDiv2 * width + (i >> 1) * 2;
                    Cb = yuv[cOff + (swap ? 0 : 1)];
                    if (Cb < 0)
                    {
                        Cb += 127;
                    } else
                    {
                        Cb -= 128;
                    }
                    Cr = yuv[cOff + (swap ? 1 : 0)];
                    if (Cr < 0)
                    {
                        Cr += 127;
                    } else
                    {
                        Cr -= 128;
                    }
                }
                int R = Y + Cr + (Cr >> 2) + (Cr >> 3) + (Cr >> 5);
                if (R < 0)
                {
                    R = 0;
                } else if (R > 255)
                {
                    R = 255;
                }
                int G = Y - (Cb >> 2) + (Cb >> 4) + (Cb >> 5) - (Cr >> 1) + (Cr >> 3) + (Cr >> 4) + (Cr >> 5);
                if (G < 0)
                {
                    G = 0;
                } else if (G > 255)
                {
                    G = 255;
                }
                int B = Y + Cb + (Cb >> 1) + (Cb >> 2) + (Cb >> 6);
                if (B < 0)
                {
                    B = 0;
                } else if (B > 255)
                {
                    B = 255;
                }
                pixPtr++;
                out[outPtr++] = (byte)R;
                out[outPtr++] = (byte)G;
                out[outPtr++] = (byte)B;
            }
        }
    }

    /**
     * Decodes YUVNV21 color space into a regular RGB format.
     *
     * @param argb Output array for RGB values.
     * @param yuv YUV byte data to decode.
     * @param width Width of image in pixels.
     * @param height Height of image in pixels.
     */
    public static void convertNV21ToARGBInt(int[] argb, byte[] yuv, int width, int height)
    {
        convertNV21ToARGBInt(argb, yuv, width, height, true);
    }

    /**
     * Decodes YUVNV21 color space into a regular RGB format.
     *
     * @param out Output array for RGB values.
     * @param yuv YUV byte data to decode.
     * @param width Width of image in pixels.
     * @param height Height of image in pixels.
     * @param swap swap U with V (default = true)
     */
    public static void convertNV21ToARGBInt(int[] out, byte[] yuv, int width, int height, boolean swap)
    {
        int sz = width * height;
        int i, j;
        int Y, Cr = 0, Cb = 0;
        for (j = 0; j < height; j++)
        {
            int pixPtr = j * width;
            final int jDiv2 = j >> 1;
            for (i = 0; i < width; i++)
            {
                Y = yuv[pixPtr];
                if (Y < 0)
                    Y += 255;
                if ((i & 0x1) != 1)
                {
                    final int cOff = sz + jDiv2 * width + (i >> 1) * 2;
                    Cb = yuv[cOff + (swap ? 0 : 1)];
                    if (Cb < 0)
                    {
                        Cb += 127;
                    } else
                    {
                        Cb -= 128;
                    }
                    Cr = yuv[cOff + (swap ? 1 : 0)];
                    if (Cr < 0)
                    {
                        Cr += 127;
                    } else
                    {
                        Cr -= 128;
                    }
                }
                int R = Y + Cr + (Cr >> 2) + (Cr >> 3) + (Cr >> 5);
                if (R < 0)
                {
                    R = 0;
                } else if (R > 255)
                {
                    R = 255;
                }
                int G = Y - (Cb >> 2) + (Cb >> 4) + (Cb >> 5) - (Cr >> 1) + (Cr >> 3) + (Cr >> 4) + (Cr >> 5);
                if (G < 0)
                {
                    G = 0;
                } else if (G > 255)
                {
                    G = 255;
                }
                int B = Y + Cb + (Cb >> 1) + (Cb >> 2) + (Cb >> 6);
                if (B < 0)
                {
                    B = 0;
                } else if (B > 255)
                {
                    B = 255;
                }
                out[pixPtr++] = 0xff000000 + (B << 16) + (G << 8) + R;
            }
        }
    }

    /**
     * Saved bitmap to external storage.
     *
     * @param bitmap Bitmap to save.
     */
    public static void saveBitmap(final Bitmap bitmap) {
        final String root =
                FileCons.SSJ_EXTERNAL_STORAGE + File.separator + "previews";
        final File myDir = new File(root);

        if (!myDir.mkdirs()) {
            Log.i("Make dir failed");
        }

        final String fname = new Date().toString().replaceAll(" ", "_").replaceAll(":", "_") + ".png";
        final File file = new File(myDir, fname);
        if (file.exists()) {
            file.delete();
        }
        try {
            final FileOutputStream out = new FileOutputStream(file);
            bitmap.compress(Bitmap.CompressFormat.PNG, 99, out);
            out.flush();
            out.close();
        } catch (final Exception e) {
            Log.e("tf_ssj", "Exception!");
        }
    }

    /**
     * Returns a transformation matrix from one reference frame into another.
     * Handles cropping (if maintaining aspect ratio is desired) and rotation.
     *
     * @param srcWidth Width of source frame.
     * @param srcHeight Height of source frame.
     * @param dstWidth Width of destination frame.
     * @param dstHeight Height of destination frame.
     * @param applyRotation Amount of rotation to apply from one frame to another.
     *  Must be a multiple of 90.
     * @param maintainAspectRatio If true, will ensure that scaling in x and y remains constant,
     * cropping the image if necessary.
     * @return The transformation fulfilling the desired requirements.
     */
    public static Matrix getTransformationMatrix(
            final int srcWidth,
            final int srcHeight,
            final int dstWidth,
            final int dstHeight,
            final int applyRotation,
            final boolean maintainAspectRatio) {
        final Matrix matrix = new Matrix();

        if (applyRotation != 0) {
            // Translate so center of image is at origin.
            matrix.postTranslate(-srcWidth / 2.0f, -srcHeight / 2.0f);

            // Rotate around origin.
            matrix.postRotate(applyRotation);
        }

        // Account for the already applied rotation, if any, and then determine how
        // much scaling is needed for each axis.
        final boolean transpose = (Math.abs(applyRotation) + 90) % 180 == 0;

        final int inWidth = transpose ? srcHeight : srcWidth;
        final int inHeight = transpose ? srcWidth : srcHeight;

        // Apply scaling if necessary.
        if (inWidth != dstWidth || inHeight != dstHeight) {
            final float scaleFactorX = dstWidth / (float) inWidth;
            final float scaleFactorY = dstHeight / (float) inHeight;

            if (maintainAspectRatio) {
                // Scale by minimum factor so that dst is filled completely while
                // maintaining the aspect ratio. Some image may fall off the edge.
                final float scaleFactor = Math.max(scaleFactorX, scaleFactorY);
                matrix.postScale(scaleFactor, scaleFactor);
            } else {
                // Scale exactly to fill dst from src.
                matrix.postScale(scaleFactorX, scaleFactorY);
            }
        }

        if (applyRotation != 0) {
            // Translate back from origin centered reference to destination frame.
            matrix.postTranslate(dstWidth / 2.0f, dstHeight / 2.0f);
        }

        return matrix;
    }

    /**
     * Converts RGB bytes to RGB ints.
     *
     * @param rgbBytes RGB color bytes.
     * @return RGB color integers.
     */
    public static int[] decodeBytes(byte[] rgbBytes, int width, int height)
    {
        int[] rgb = new int[width * height];

        for (int i = 0; i < width * height; i++)
        {
            int r = rgbBytes[i * 3];
            int g = rgbBytes[i * 3 + 1];
            int b = rgbBytes[i * 3 + 2];

            if (r < 0)
                r += 256;
            if (g < 0)
                g += 256;
            if (b < 0)
                b += 256;

            rgb[i] = 0xff000000 | (r << 16) | (g << 8) | b;
        }

        return rgb;
    }

    /**
     * Converts bitmap to corresponding byte array and writes it
     * to the output buffer.
     *
     * @param bitmap Bitmap to convert to byte array.
     * @param intOutput Integer output buffer
     * @param byteOutput Byte output buffer.
     */
    public static void convertBitmapToByteArray(Bitmap bitmap, int[] intOutput, byte[] byteOutput)
    {
        bitmap.getPixels(intOutput, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight());

        for (int i = 0; i < bitmap.getWidth() * bitmap.getHeight(); ++i)
        {
            final int pixel = intOutput[i];
            byteOutput[i * 3] = (byte)((pixel >> 16) & 0xFF);
            byteOutput[i * 3 + 1] = (byte)((pixel >> 8) & 0xFF);
            byteOutput[i * 3 + 2] = (byte)(pixel & 0xFF);
        }
    }

	public static void convertRGBToARGBInt(int[] argb, byte[] rgb, int width, int height)
    {
        int cnt_out = 0;
        int cnt_in = 0;
        int r,g,b;

        for(int i = 0; i < width; i++)
        {
            for (int j = 0; j < height; j++)
            {
                r = rgb[cnt_in++];
                g = rgb[cnt_in++];
                b = rgb[cnt_in++];

                if (r < 0) r += 256;
                if (g < 0) g += 256;
                if (b < 0) b += 256;

                argb[cnt_out++] = 0xff000000 | (r << 16) | (g << 8) | b;
            }
        }
    }

    /**
     * Decodes YUV frame to a RGB buffer
     *
     * @param rgba     int[]
     * @param yuv420sp byte[]
     * @param width    width
     * @param height   height
     */
    public static void decodeYV12PackedSemi(int[] rgba, byte[] yuv420sp, int width, int height)
    {
        //@todo untested
        final int frameSize = width * height;
        int r, g, b, y1192, y, i, uvp, u, v;
        for (int j = 0, yp = 0; j < height; j++)
        {
            uvp = frameSize + (j >> 1) * width;
            u = 0;
            v = 0;
            for (i = 0; i < width; i++, yp++)
            {
                y = (0xff & ((int) yuv420sp[yp])) - 16;
                if (y < 0)
                    y = 0;
                if ((i & 1) == 0)
                {
                    v = (0xff & yuv420sp[uvp++]) - 128;
                    u = (0xff & yuv420sp[uvp++]) - 128;
                }
                y1192 = 1192 * y;
                r = (y1192 + 1634 * v);
                g = (y1192 - 833 * v - 400 * u);
                b = (y1192 + 2066 * u);
                //
                r = Math.max(0, Math.min(r, 262143));
                g = Math.max(0, Math.min(g, 262143));
                b = Math.max(0, Math.min(b, 262143));
                // rgb[yp] = 0xff000000 | ((r << 6) & 0xff0000) | ((g >> 2) &
                // 0xff00) | ((b >> 10) & 0xff);
                // rgba, divide 2^10 ( >> 10)
                rgba[yp] = ((r << 14) & 0xff000000) | ((g << 6) & 0xff0000)
                        | ((b >> 2) | 0xff00);
            }
        }
    }
}