/*
 * Copyright (C) 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.graphics;

import android.annotation.FlaggedApi;
import android.annotation.FloatRange;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Parcel;
import android.os.Parcelable;

import com.android.graphics.hwui.flags.Flags;

import libcore.util.NativeAllocationRegistry;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/**
 * Gainmap represents a mechanism for augmenting an SDR image to produce an HDR one with variable
 * display adjustment capability. It is a combination of a set of metadata describing how to apply
 * the gainmap, as well as either a 1 (such as {@link android.graphics.Bitmap.Config#ALPHA_8} or 3
 * (such as {@link android.graphics.Bitmap.Config#ARGB_8888} with the alpha channel ignored)
 * channel Bitmap that represents the gainmap data itself.
 * <p>
 * When rendering to an {@link android.content.pm.ActivityInfo#COLOR_MODE_HDR} activity, the
 * hardware accelerated {@link Canvas} will automatically apply the gainmap when sufficient
 * HDR headroom is available.
 *
 * <h3>Gainmap Structure</h3>
 *
 * The logical whole of a gainmap'd image consists of a base Bitmap that represents the original
 * image as would be displayed without gainmap support in addition to a gainmap with a second
 * enhancement image. In the case of a JPEG, the base image would be the typical 8-bit SDR image
 * that the format is commonly associated with. The gainmap image is embedded alongside the base
 * image, often at a lower resolution (such as 1/4th), along with some metadata to describe
 * how to apply the gainmap. The gainmap image itself is then a greyscale image representing
 * the transformation to apply onto the base image to reconstruct an HDR rendition of it.
 * <p>
 * As such these "gainmap images" consist of 3 parts - a base {@link Bitmap} with a
 * {@link Bitmap#getGainmap()} that returns an instance of this class which in turn contains
 * the enhancement layer represented as another Bitmap, accessible via {@link #getGainmapContents()}
 *
 * <h3>Applying a gainmap manually</h3>
 *
 * When doing custom rendering such as to an OpenGL ES or Vulkan context, the gainmap is not
 * automatically applied. In such situations, the following steps are appropriate to render the
 * gainmap in combination with the base image.
 * <p>
 * Suppose our display has HDR to SDR ratio of H, and we wish to display an image with gainmap on
 * this display. Let B be the pixel value from the base image in a color space that has the
 * primaries of the base image and a linear transfer function. Let G be the pixel value from the
 * gainmap. Let D be the output pixel in the same color space as B. The value of D is computed
 * as follows:
 * <p>
 * First, let W be a weight parameter determining how much the gainmap will be applied.
 * <pre class="prettyprint">
 *   W = clamp((log(H)                      - log(minDisplayRatioForHdrTransition)) /
 *             (log(displayRatioForFullHdr) - log(minDisplayRatioForHdrTransition), 0, 1)</pre>
 *
 * Next, let L be the gainmap value in log space. We compute this from the value G that was
 * sampled from the texture as follows:
 * <pre class="prettyprint">
 *   L = mix(log(ratioMin), log(ratioMax), pow(G, gamma))</pre>
 * Finally, apply the gainmap to compute D, the displayed pixel. If the base image is SDR then
 * compute:
 * <pre class="prettyprint">
 *   D = (B + epsilonSdr) * exp(L * W) - epsilonHdr</pre>
 * <p>
 * In the above math, log() is a natural logarithm and exp() is natural exponentiation. The base
 * for these functions cancels out and does not affect the result, so other bases may be used
 * if preferred.
 */
@android.ravenwood.annotation.RavenwoodKeepWholeClass
public final class Gainmap implements Parcelable {

    /** @hide */
    @Retention(RetentionPolicy.SOURCE)
    @IntDef(prefix = {"GAINMAP_DIRECTION_"},
            value = {GAINMAP_DIRECTION_SDR_TO_HDR,
                     GAINMAP_DIRECTION_HDR_TO_SDR})
    public @interface GainmapDirection {}

    /**
     * The gainmap will be applied as if the base image were SDR, and fully applying the gainmap
     * results in an HDR image.
     */
    @FlaggedApi(Flags.FLAG_ISO_GAINMAP_APIS)
    public static final int GAINMAP_DIRECTION_SDR_TO_HDR = 0;

    /**
     * The gainmap will be applied as if the base image were HDR, and fully applying the gainmap
     * results in an SDR image.
     */
    @FlaggedApi(Flags.FLAG_ISO_GAINMAP_APIS)
    public static final int GAINMAP_DIRECTION_HDR_TO_SDR = 1;

    // Use a Holder to allow static initialization of Gainmap in the boot image.
    private static class NoImagePreloadHolder {
        public static final NativeAllocationRegistry sRegistry =
                NativeAllocationRegistry.createMalloced(
                        Gainmap.class.getClassLoader(), nGetFinalizer());
    }

    final long mNativePtr;
    private Bitmap mGainmapContents;

    // called from JNI
    private Gainmap(Bitmap gainmapContents, long nativeGainmap) {
        if (nativeGainmap == 0) {
            throw new RuntimeException("internal error: native gainmap is 0");
        }

        mNativePtr = nativeGainmap;
        setGainmapContents(gainmapContents);

        NoImagePreloadHolder.sRegistry.registerNativeAllocation(this, nativeGainmap);
    }

    /**
     * Creates a gainmap from a given Bitmap. The caller is responsible for setting the various
     * fields to the desired values. The defaults are as follows:
     * <ul>
     *     <li>Ratio min is 1f, 1f, 1f</li>
     *     <li>Ratio max is 2f, 2f, 2f</li>
     *     <li>Gamma is 1f, 1f, 1f</li>
     *     <li>Epsilon SDR is 0f, 0f, 0f</li>
     *     <li>Epsilon HDR is 0f, 0f, 0f</li>
     *     <li>Display ratio SDR is 1f</li>
     *     <li>Display ratio HDR is 2f</li>
     * </ul>
     * It is strongly recommended that at least the ratio max and display ratio HDR are adjusted
     * to better suit the given gainmap contents.
     */
    public Gainmap(@NonNull Bitmap gainmapContents) {
        this(gainmapContents, nCreateEmpty());
    }

    /**
     * Creates a new gainmap using the provided gainmap as the metadata source and the provided
     * bitmap as the replacement for the gainmapContents
     */
    public Gainmap(@NonNull Gainmap gainmap, @NonNull Bitmap gainmapContents) {
        this(gainmapContents, nCreateCopy(gainmap.mNativePtr));
    }

    /**
     * @hide
     */
    public Gainmap asShared() {
        final Bitmap sharedContents = mGainmapContents.asShared();
        if (sharedContents == mGainmapContents) {
            return this;
        } else {
            return new Gainmap(sharedContents, nCreateCopy(mNativePtr));
        }
    }

    /**
     * @return Returns the image data of the gainmap represented as a Bitmap. This is represented
     * as a Bitmap for broad API compatibility, however certain aspects of the Bitmap are ignored
     * such as {@link Bitmap#getColorSpace()} or {@link Bitmap#getGainmap()} as they are not
     * relevant to the gainmap's enhancement layer.
     */
    @NonNull
    public Bitmap getGainmapContents() {
        return mGainmapContents;
    }

    /**
     * Sets the image data of the gainmap. This is the 1 or 3 channel enhancement layer to apply
     * to the base image. This is represented as a Bitmap for broad API compatibility, however
     * certain aspects of the Bitmap are ignored such as {@link Bitmap#getColorSpace()} or
     * {@link Bitmap#getGainmap()} as they are not relevant to the gainmap's enhancement layer.
     *
     * @param bitmap The non-null bitmap to set as the gainmap's contents
     */
    public void setGainmapContents(@NonNull Bitmap bitmap) {
        // TODO: Validate here or leave native-side?
        if (bitmap.isRecycled()) throw new IllegalArgumentException("Bitmap is recycled");
        nSetBitmap(mNativePtr, bitmap);
        mGainmapContents = bitmap;
    }

    /**
     * Sets the gainmap ratio min. For single-plane gainmaps, r, g, and b should be the same.
     */
    public void setRatioMin(float r, float g, float b) {
        nSetRatioMin(mNativePtr, r, g, b);
    }

    /**
     * Gets the gainmap ratio max. For single-plane gainmaps, all 3 components should be the
     * same. The components are in r, g, b order.
     */
    @NonNull
    public float[] getRatioMin() {
        float[] ret = new float[3];
        nGetRatioMin(mNativePtr, ret);
        return ret;
    }

    /**
     * Sets the gainmap ratio max. For single-plane gainmaps, r, g, and b should be the same.
     */
    public void setRatioMax(float r, float g, float b) {
        nSetRatioMax(mNativePtr, r, g, b);
    }

    /**
     * Gets the gainmap ratio max. For single-plane gainmaps, all 3 components should be the
     * same. The components are in r, g, b order.
     */
    @NonNull
    public float[] getRatioMax() {
        float[] ret = new float[3];
        nGetRatioMax(mNativePtr, ret);
        return ret;
    }

    /**
     * Sets the gainmap gamma. For single-plane gainmaps, r, g, and b should be the same.
     */
    public void setGamma(float r, float g, float b) {
        nSetGamma(mNativePtr, r, g, b);
    }

    /**
     * Gets the gainmap gamma. For single-plane gainmaps, all 3 components should be the
     * same. The components are in r, g, b order.
     */
    @NonNull
    public float[] getGamma() {
        float[] ret = new float[3];
        nGetGamma(mNativePtr, ret);
        return ret;
    }

    /**
     * Sets the sdr epsilon which is used to avoid numerical instability.
     * For single-plane gainmaps, r, g, and b should be the same.
     */
    public void setEpsilonSdr(float r, float g, float b) {
        nSetEpsilonSdr(mNativePtr, r, g, b);
    }

    /**
     * Gets the sdr epsilon. For single-plane gainmaps, all 3 components should be the
     * same. The components are in r, g, b order.
     */
    @NonNull
    public float[] getEpsilonSdr() {
        float[] ret = new float[3];
        nGetEpsilonSdr(mNativePtr, ret);
        return ret;
    }

    /**
     * Sets the hdr epsilon which is used to avoid numerical instability.
     * For single-plane gainmaps, r, g, and b should be the same.
     */
    public void setEpsilonHdr(float r, float g, float b) {
        nSetEpsilonHdr(mNativePtr, r, g, b);
    }

    /**
     * Gets the hdr epsilon. For single-plane gainmaps, all 3 components should be the
     * same. The components are in r, g, b order.
     */
    @NonNull
    public float[] getEpsilonHdr() {
        float[] ret = new float[3];
        nGetEpsilonHdr(mNativePtr, ret);
        return ret;
    }

    /**
     * Sets the hdr/sdr ratio at which point applying the gainmap results in an HDR rendition.
     * @param max The hdr/sdr ratio at which point applying the gainmap results in an HDR rendition.
     * Must be >= 1.0f
     */
    public void setDisplayRatioForFullHdr(@FloatRange(from = 1.0f) float max) {
        if (!Float.isFinite(max) || max < 1f) {
            throw new IllegalArgumentException(
                    "setDisplayRatioForFullHdr must be >= 1.0f, got = " + max);
        }
        nSetDisplayRatioHdr(mNativePtr, max);
    }

    /**
     * Gets the hdr/sdr ratio at which point applying the gainmap results in an HDR rendition
     */
    @NonNull
    public float getDisplayRatioForFullHdr() {
        return nGetDisplayRatioHdr(mNativePtr);
    }

    /**
     * Sets the hdr/sdr ratio below which applying the gainmap results in an SDR rendition.
     * @param min The minimum hdr/sdr ratio at which point applying the gainmap results in an SDR
     * rendition. Must be >= 1.0f
     */
    public void setMinDisplayRatioForHdrTransition(@FloatRange(from = 1.0f) float min) {
        if (!Float.isFinite(min) || min < 1f) {
            throw new IllegalArgumentException(
                    "setMinDisplayRatioForHdrTransition must be >= 1.0f, got = " + min);
        }
        nSetDisplayRatioSdr(mNativePtr, min);
    }

    /**
     * Gets the hdr/sdr ratio below which applying the gainmap results in an SDR rendition.
     */
    @NonNull
    public float getMinDisplayRatioForHdrTransition() {
        return nGetDisplayRatioSdr(mNativePtr);
    }

    /**
     * Sets the colorspace that the gainmap math should be applied in.
     * Only the primaries are what is relevant for applying the gainmap. The transfer and range
     * characteritics are ignored.
     *
     * If the supplied ColorSpace is null, then applying the gainmap will be done using the color
     * gamut of the base image.
     */
    @FlaggedApi(Flags.FLAG_ISO_GAINMAP_APIS)
    public void setAlternativeImagePrimaries(@Nullable ColorSpace colorSpace) {
        long colorSpaceInstance = colorSpace == null ? 0 : colorSpace.getNativeInstance();
        nSetAlternativeColorSpace(mNativePtr, colorSpaceInstance);
    }

    /**
     * Gets the colorspace that the gainmap math should be applied in.
     * Only the primaries are what is relevant for applying the gainmap. The transfer and range
     * characteritics are ignored.
     *
     * If the returned ColorSpace is null, then applying the gainmap will be done using the color
     * gamut of the base image.
     */
    @FlaggedApi(Flags.FLAG_ISO_GAINMAP_APIS)
    @Nullable
    public ColorSpace getAlternativeImagePrimaries() {
        return nGetAlternativeColorSpace(mNativePtr);
    }

    /**
     * Sets the direction that the gainmap math should be applied in.
     */
    @FlaggedApi(Flags.FLAG_ISO_GAINMAP_APIS)
    public void setGainmapDirection(@GainmapDirection int direction) {
        if (direction != GAINMAP_DIRECTION_SDR_TO_HDR
                && direction != GAINMAP_DIRECTION_HDR_TO_SDR) {
            throw new IllegalArgumentException("Invalid gainmap direction: " + direction);
        }
        nSetDirection(mNativePtr, direction);
    }

    /**
     * Gets the direction that the gainmap math should be applied in.
     */
    @FlaggedApi(Flags.FLAG_ISO_GAINMAP_APIS)
    public @GainmapDirection int getGainmapDirection() {
        return nGetDirection(mNativePtr);
    }


    /**
     * No special parcel contents.
     */
    @Override
    public int describeContents() {
        return 0;
    }

    /**
     * Write the gainmap to the parcel.
     *
     * @param dest Parcel object to write the gainmap data into
     * @param flags Additional flags about how the object should be written.
     */
    @Override
    public void writeToParcel(@NonNull Parcel dest, int flags) {
        if (mNativePtr == 0) {
            throw new IllegalStateException("Cannot be written to a parcel");
        }
        dest.writeTypedObject(mGainmapContents, flags);
        // write gainmapinfo into parcel
        nWriteGainmapToParcel(mNativePtr, dest);
    }

    public static final @NonNull Parcelable.Creator<Gainmap> CREATOR =
            new Parcelable.Creator<Gainmap>() {
            /**
             * Rebuilds a gainmap previously stored with writeToParcel().
             *
             * @param in Parcel object to read the gainmap from
             * @return a new gainmap created from the data in the parcel
             */
            public Gainmap createFromParcel(Parcel in) {
                Gainmap gm = new Gainmap(in.readTypedObject(Bitmap.CREATOR));
                // read gainmapinfo from parcel
                nReadGainmapFromParcel(gm.mNativePtr, in);
                return gm;
            }

            public Gainmap[] newArray(int size) {
                return new Gainmap[size];
            }
        };

    private static native long nGetFinalizer();
    private static native long nCreateEmpty();
    private static native long nCreateCopy(long source);

    private static native void nSetBitmap(long ptr, Bitmap bitmap);

    private static native void nSetRatioMin(long ptr, float r, float g, float b);
    private static native void nGetRatioMin(long ptr, float[] components);

    private static native void nSetRatioMax(long ptr, float r, float g, float b);
    private static native void nGetRatioMax(long ptr, float[] components);

    private static native void nSetGamma(long ptr, float r, float g, float b);
    private static native void nGetGamma(long ptr, float[] components);

    private static native void nSetEpsilonSdr(long ptr, float r, float g, float b);
    private static native void nGetEpsilonSdr(long ptr, float[] components);

    private static native void nSetEpsilonHdr(long ptr, float r, float g, float b);
    private static native void nGetEpsilonHdr(long ptr, float[] components);

    private static native void nSetDisplayRatioHdr(long ptr, float max);
    private static native float nGetDisplayRatioHdr(long ptr);

    private static native void nSetDisplayRatioSdr(long ptr, float min);
    private static native float nGetDisplayRatioSdr(long ptr);
    private static native void nSetAlternativeColorSpace(long ptr, long colorSpacePtr);
    private static native ColorSpace nGetAlternativeColorSpace(long ptr);
    private static native void nSetDirection(long ptr, int direction);
    private static native int nGetDirection(long ptr);
    private static native void nWriteGainmapToParcel(long ptr, Parcel dest);
    private static native void nReadGainmapFromParcel(long ptr, Parcel src);
}
