/*
 * Copyright 2025 Google LLC.
 *
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */

#include "include/core/SkBitmap.h"
#include "include/core/SkColorFilter.h"
#include "include/effects/SkRuntimeEffect.h"
#include "include/private/SkHdrMetadata.h"
#include "src/codec/SkCodecPriv.h"
#include "src/codec/SkHdrAgtmPriv.h"

namespace {

// The maximum and minimum HDR headroom values allowed by the specification.
static constexpr float kMinHdrHeadroom = 0.f;
static constexpr float kMaxHdrHeadroom = 6.f;

// The maximum linear value is exp2(kMaxHdrHeadroom) =  64.
static constexpr float kMaxLinearHdrHeadroom = 64.f;

static bool in_unit_interval(float x) {
  return x >= 0.f && x <= 1.f;
}

// AGTM tone mapping shader.
static constexpr char gAgtmSKSL[] =
    "uniform half scale_factor;"       // The scale to apply in linear space
    "uniform shader curve_xym;"        // The texture containing control points.
    "uniform half weight_i;"           // The weight of gain curve "i"
    "uniform half4 mix_rgbx_i;"        // The red,green,blue mixing coefficients.
    "uniform half4 mix_Mmcx_i;"        // The max,min,component mixing coefficients.
    "uniform half curve_texcoord_y_i;" // The y texture coordinate at which to sample curve_xym.
    "uniform half curve_N_cp_i;"       // The number of control points.
    "uniform half weight_j;"           // All the same parameters, for gain curve "j"
    "uniform half4 mix_rgbx_j;"
    "uniform half4 mix_Mmcx_j;"
    "uniform half curve_texcoord_y_j;"
    "uniform half curve_N_cp_j;"

     // Shader equivalent of AgtmHelpers::EvaluateComponentMixingFunction.
    "half3 EvalComponentMixing(half3 color, half4 rgbx, half4 Mmcx) {"
      "half common = dot(rgbx.rgb, color) +"
                    "Mmcx[0] * max(max(color.r, color.g), color.b) +"
                    "Mmcx[1] * min(min(color.r, color.g), color.b);"
      "return Mmcx[2] * color + half3(common);"
    "}"

     // Shader equivalent of AgtmHelpers::EvaluateGainCurve.
    "half EvalGainCurve(half x, half curve_texcoord_y, half curve_N_cp) {"
       // Handle points to the left of the first control point.
      "half c_min = 0.0;"
      "half4 xym_min = curve_xym.eval(half2(c_min + 0.5, curve_texcoord_y));"
      "if (x <= xym_min.x) {"
        "return xym_min.y;"
      "}"
       // Handle points after the last control point.
      "half c_max = curve_N_cp - 1.0;"
      "half4 xym_max = curve_xym.eval(half2(c_max + 0.5, curve_texcoord_y));"
      "if (x >= xym_max.x) {"
        "return xym_max.y + log2(xym_max.x / x);"
      "}"
       // Binary search for the interval containing x. This will require sampling at most
       // log2(32)=5 more control points.
      "for (int step = 0; step < 5; ++step) {"
         // Early-out if we've already found the interval.
        "if (c_max - c_min <= 1.0) {"
          "break;"
        "}"
         // Test the midpoint and replace one of the endpoints with it.
        "half c_mid = ceil(0.5 * (c_min + c_max));"
        "half4 xym_mid = curve_xym.eval(half2(c_mid + 0.5, curve_texcoord_y));"
        "if (x == xym_mid.x) {"
           // If we hit a control point exactly, then just return.
          "return xym_mid.y;"
        "} else if (x < xym_mid.x) {"
        "c_max = c_mid;"
          "xym_max = xym_mid;"
        "} else {"
        "c_min = c_mid;"
          "xym_min = xym_mid;"
        "}"
      "}"
       // Evaluate the cubic.
      "half h = xym_max.x - xym_min.x;"
      "half mHat_min = xym_min.z * h;"
      "half mHat_max = xym_max.z * h;"
      "half c3 =  2.0 * xym_min.y + mHat_min - 2.0 * xym_max.y + mHat_max;"
      "half c2 = -3.0 * xym_min.y + 3.0 * xym_max.y - 2.0 * mHat_min - mHat_max;"
      "half c1 = mHat_min;"
      "half c0 = xym_min.y;"
      "half t = (x - xym_min.x) / h;"
      "return ((c3*t + c2)*t + c1)*t + c0;"
    "}"

     // Shader equivalent of AgtmHelpers::EvaluateColorGainFunction.
    "half3 EvalColorGainFunction(half3 color,"
                                "half4 mix_rgbx, half4 mix_Mmcx,"
                                "float curve_texcoord_y, float curve_N_cp) {"
      "half3 M = EvalComponentMixing(color, mix_rgbx, mix_Mmcx);"
      "if (mix_Mmcx.b == 0.0) {"
         // If the kComponent coefficient is zero, only evalute the curve once.
        "return half3(EvalGainCurve(M.r, curve_texcoord_y, curve_N_cp));"
      "}"
      "return half3(EvalGainCurve(M.r, curve_texcoord_y, curve_N_cp),"
                   "EvalGainCurve(M.g, curve_texcoord_y, curve_N_cp),"
                   "EvalGainCurve(M.b, curve_texcoord_y, curve_N_cp));"
    "}"

     // Shader equivalent of AgtmHelpers::ApplyGain.
    "half4 main(half4 color) {"
      "color.rgb *= scale_factor;"
      "if (weight_i > 0.0) {"
         // Unpremultiply alpha is needed.
        "float a_inv = (color.a == 0.0) ? 1.0 : 1.0 / color.a;"
        "half3 G = half3(0.0);"
        "G += weight_i * EvalColorGainFunction(color.rgb * a_inv,"
                                              "mix_rgbx_i, mix_Mmcx_i,"
                                              "curve_texcoord_y_i, curve_N_cp_i);"
        "if (weight_j > 0.0) {"
          "G += weight_j * EvalColorGainFunction(color.rgb * a_inv,"
                                                "mix_rgbx_j, mix_Mmcx_j,"
                                                "curve_texcoord_y_j, curve_N_cp_j);"
        "}"
        "color.rgb *= exp2(G);"
      "}"
      "return color;"
    "}";

static sk_sp<SkRuntimeEffect> agtm_runtime_effect() {
    auto init_lambda = []() {
        auto result = SkRuntimeEffect::MakeForColorFilter(SkString(gAgtmSKSL), {});
        SkASSERTF(result.effect, "Agtm shader log:\n%s\n", result.errorText.c_str());
        return result.effect.release();
    };
    static SkRuntimeEffect* effect = init_lambda();
    return sk_ref_sp(effect);
}

}  // namespace

namespace skhdr {

SkColor4f AgtmHelpers::EvaluateComponentMixingFunction(
        const AdaptiveGlobalToneMap::ComponentMixingFunction& mix, const SkColor4f& c) {
    // This implements that math in Formula (9).
    float common = mix.fRed * c.fR + mix.fGreen * c.fG + mix.fBlue * c.fB  +
                   mix.fMax * std::max(std::max(c.fR, c.fG), c.fB) +
                   mix.fMin * std::min(std::max(c.fR, c.fG), c.fB);

    // Optimization for when all components are the same.
    if (mix.fComponent == 0.f) {
        return {common, common, common, c.fA};
    }

    // Formula (10).
    return {mix.fComponent * c.fR + common,
            mix.fComponent * c.fG + common,
            mix.fComponent * c.fB + common,
            c.fA};
}

namespace AgtmHelpers {

float EvaluateGainCurve(const AdaptiveGlobalToneMap::GainCurve& gainCurve, float x) {
    auto& cp = gainCurve.fControlPoints;
    size_t N = cp.size();

    // This implements that math in Formula (11).
    SkASSERT(N > 0 && N <= 32);

    // Handle points off of the left endpoint.
    size_t i = 0;
    if (x <= cp[i].fX) {
        return cp[i].fY;
    }

    // Handle points off of the right endpoint.
    size_t j = N - 1;
    if (x >= cp[j].fX) {
        return cp[j].fY + std::log2(cp[j].fX / x);
    }

    // Binary search for i, j bracket in which we find x.
    while (j - i > 1) {
        size_t m = (i + j) / 2;
        if (x < cp[m].fX) {
            j = m;
        } else {
            i = m;
        }
    }

    // Cache short names for the parameters for computing the cubic coefficients.
    const float x_i = cp[i].fX;
    const float y_i = cp[i].fY;
    const float x_j = cp[j].fX;
    const float y_j = cp[j].fY;
    const float h_i = x_j - x_i;
    const float mHat_i = cp[i].fM * h_i;
    const float mHat_j = cp[j].fM * h_i;

    // Handle intervals that are a point.
    if (h_i == 0.f) {
        return y_i;
    }

    // Compute the coefficients and evaluate the polynomial.
    const float c3 =  2.f * y_i + mHat_i - 2.f * y_j + mHat_j;
    const float c2 = -3.f * y_i + 3.f * y_j - 2.f * mHat_i - mHat_j;
    const float c1 = mHat_i;
    const float c0 = y_i;
    const float t = (x - x_i) / h_i;

    return ((c3*t + c2)*t + c1)*t + c0;
}

SkColor4f EvaluateColorGainFunction(
        const AdaptiveGlobalToneMap::ColorGainFunction& gain, const SkColor4f& c) {
    SkColor4f m = EvaluateComponentMixingFunction(gain.fComponentMixing, c);
    SkColor4f result = {0.f, 0.f, 0.f, c.fA};
    result.fR = EvaluateGainCurve(gain.fGainCurve, m.fR);
    if (m.fR == m.fG && m.fG == m.fB) {
      result.fG = result.fR;
      result.fB = result.fR;
    } else {
      result.fG = EvaluateGainCurve(gain.fGainCurve, m.fG);
      result.fB = EvaluateGainCurve(gain.fGainCurve, m.fB);
    }
    return result;
}

void PopulateSlopeFromPCHIP(AdaptiveGlobalToneMap::GainCurve& gainCurve) {
    auto& cp = gainCurve.fControlPoints;
    size_t N = cp.size();

    // Compute the interval width (h) and piecewise linear slope (s).
    float s[AdaptiveGlobalToneMap::GainCurve::kMaxNumControlPoints];
    float h[AdaptiveGlobalToneMap::GainCurve::kMaxNumControlPoints];
    for (size_t i = 0; i < N - 1; ++i) {
        h[i] = cp[i+1].fX - cp[i].fX;
    }
    for (size_t i = 0; i < N - 1; ++i) {
        s[i] = (cp[i+1].fY - cp[i].fY) / h[i];
    }

    // Handle the left and right control points.
    if (N >= 3) {
        // Formula (C.7) and Formula (C.8).
        cp[0].fM   = ((2 * h[0]   + h[1]  ) * s[0]   - h[0]   * s[1]  ) / (h[0]   + h[1]  );
        cp[N-1].fM = ((2 * h[N-2] + h[N-3]) * s[N-2] - h[N-2] * s[N-3]) / (h[N-2] + h[N-3]);
    } else if (N == 2) {
        cp[0].fM   = s[0];
        cp[N-1].fM = s[0];
    } else {
        cp[0].fM   = 0.f;
        cp[N-1].fM = 0.f;
    }

    // Populate internal control points.
    for (size_t i = 1; i <= N - 2; ++i) {
        // Formula (C.9).
        if (s[i-1] * s[i] < 0.f) {
            cp[i].fM = 0.f;
        } else {
            float num = 3 * (h[i-1] + h[i]) * s[i-1] * s[i];
            float den = (2 * h[i-1] + h[i]) * s[i-1] + (h[i-1] + 2 * h[i]) * s[i];
            cp[i].fM = num / den;
        }
    }
}

sk_sp<SkImage>
MakeGainCurveXYMImage(const AdaptiveGlobalToneMap::HeadroomAdaptiveToneMap& hatm) {
    if (hatm.fAlternateImages.empty()) {
        return nullptr;
    }
    size_t maxNumControlPoints = 1;
    for (const auto& alt : hatm.fAlternateImages) {
        maxNumControlPoints = std::max(maxNumControlPoints,
                                       alt.fColorGainFunction.fGainCurve.fControlPoints.size());
    }

    // Write the X, Y, and M values of the control points into the colors of the rows.
    SkBitmap bm32;
    bm32.allocPixels(SkImageInfo::Make(
            AdaptiveGlobalToneMap::GainCurve::kMaxNumControlPoints, hatm.fAlternateImages.size(),
            kRGBA_F32_SkColorType, kPremul_SkAlphaType));
    for (size_t a = 0; a < hatm.fAlternateImages.size(); ++a) {
        const auto& alt = hatm.fAlternateImages[a];
        const auto& curve = alt.fColorGainFunction.fGainCurve;
        for (size_t c = 0; c < curve.fControlPoints.size(); ++c) {
            float* xymX = reinterpret_cast<float*>(bm32.getAddr(c, a));
            xymX[0] = curve.fControlPoints[c].fX;
            xymX[1] = curve.fControlPoints[c].fY;
            xymX[2] = curve.fControlPoints[c].fM;
            xymX[3] = 1.f;
        }
    }

    // Convert from F32 to F16 for use on the GPU.
    SkBitmap bm16;
    bm16.allocPixels(bm32.info().makeColorType(kRGBA_F16_SkColorType));
    if (!bm32.readPixels(bm16.pixmap())) {
        return nullptr;
    }
    bm16.setImmutable();
    return SkImages::RasterFromBitmap(bm16);
}

void PopulateUsingRwtmo(AdaptiveGlobalToneMap::HeadroomAdaptiveToneMap& hatm) {
    hatm.fGainApplicationSpacePrimaries = SkNamedPrimaries::kRec2020;

    if (hatm.fBaselineHdrHeadroom == 0.f) {
        hatm.fAlternateImages.clear();
        return;
    }

    // Set the two alternate image headrooms using Formula (C.1).
    hatm.fAlternateImages.resize(2);
    hatm.fAlternateImages[0].fHdrHeadroom = 0.f;
    hatm.fAlternateImages[1].fHdrHeadroom =
        std::log2(8.f / 3.f) * std::min(hatm.fBaselineHdrHeadroom / std::log2(1000/203.f), 1.f);

    for (size_t a = 0; a < hatm.fAlternateImages.size(); ++a) {
        auto& gain = hatm.fAlternateImages[a].fColorGainFunction;
        gain = AdaptiveGlobalToneMap::ColorGainFunction();

        // Use maxRGB for applying the curve.
        gain.fComponentMixing.fMax = 1.f;

        // Compute the image of white under the tone mapping from Formula (C.2).
        const float yWhite =
            (a == 1) ? 1.f
                     : 1.f - 0.5f * std::min(hatm.fBaselineHdrHeadroom / std::log2(1000/203.f), 1.f);

        // Compute the Bezier control points using Formula (C.3).
        const float kappa = 0.65f;
        const float xKnee = 1.f;
        const float yKnee = yWhite;
        const float xMax = std::exp2(hatm.fBaselineHdrHeadroom);
        const float yMax = std::exp2(hatm.fAlternateImages[a].fHdrHeadroom);
        const float xMid = (1.f - kappa) * xKnee + kappa * (xKnee * yMax / yKnee);
        const float yMid = (1.f - kappa) * yKnee + kappa * yMax;

        // Compute the cubic coefficients using Formula (C.5).
        const float xA = xKnee - 2.f * xMid + xMax;
        const float yA = yKnee - 2.f * yMid + yMax;
        const float xB = 2.f * xMid - 2.f * xKnee;
        const float yB = 2.f * yMid - 2.f * yKnee;
        const float xC = xKnee;
        const float yC = yKnee;

        auto& cubic = gain.fGainCurve;
        cubic.fControlPoints.resize(8);
        for (size_t c = 0; c < cubic.fControlPoints.size(); ++c) {
            // Compute the linear domain curve values using Formula (C.4).
            const float t = c / (cubic.fControlPoints.size() - 1.f);
            const float x = xC + t * (xB + t * xA);
            const float y = yC + t * (yB + t * yA);
            const float m = (2.f * yA * t + yB) / (2.f * xA * t + xB);

            // Compute the log domain curve values using Formula (C.6).
            cubic.fControlPoints[c].fX = x;
            cubic.fControlPoints[c].fY = std::log2(y / x);
            cubic.fControlPoints[c].fM = (x * m - y) / (std::log(2.f) * x * y);
        }
    }
}

Weighting ComputeWeighting(const AdaptiveGlobalToneMap::HeadroomAdaptiveToneMap& hatm,
                           float targetedHdrHeadroom) {
    Weighting result;

    // Create the list of HDR headrooms including the baseline image and all alternate images, as
    // described Clause 6.2.5 Computation of the headroom-adaptive tone map.

    // Let N be the length of the combined list.
    size_t N = 0;

    // Let H be the sorted list of HDR headrooms.
    float H[AdaptiveGlobalToneMap::HeadroomAdaptiveToneMap::kMaxNumAlternateImages + 1];

    // Let indices list the index of each entry of H in fAlternateHdrHeadroom. The index for
    // fBaselineHdrHeadroom is Weighting::kInvalidIndex.
    size_t indices[AdaptiveGlobalToneMap::HeadroomAdaptiveToneMap::kMaxNumAlternateImages + 1];
    for (size_t i = 0; i < hatm.fAlternateImages.size(); ++i) {
        if (N == i && hatm.fBaselineHdrHeadroom < hatm.fAlternateImages[i].fHdrHeadroom) {
            // Insert the baseline HDR headroom before the indices as they are visited.
            indices[N] = Weighting::kInvalidIndex;
            H[N++] = hatm.fBaselineHdrHeadroom;
        }
        indices[N] = i;
        H[N++] = hatm.fAlternateImages[i].fHdrHeadroom;
    }
    if (N == hatm.fAlternateImages.size()) {
        // Insert the baseline HDR headroom at the end if it has not yet been inserted.
        indices[N] = Weighting::kInvalidIndex;
        H[N++] = hatm.fBaselineHdrHeadroom;
    }

    // Find the indices for the contributing images.
    if (targetedHdrHeadroom <= H[0]) {
        // One case of Formula (2), for the left endpoint.
        result.fWeight[0] = 1.f;
        result.fAlternateImageIndex[0] = indices[0];
    } else if (targetedHdrHeadroom >= H[N-1]) {
        // The other case of Formula (2), for the right endpoint.
        result.fWeight[0] = 1.f;
        result.fAlternateImageIndex[0] = indices[N-1];
    } else {
        // The case of Formula (3).
        size_t i = 0;
        for (i = 0; i < N - 1; ++i) {
            if (H[i] <= targetedHdrHeadroom && targetedHdrHeadroom <= H[i+1]) {
                break;
            }
        }
        result.fWeight[0] = (targetedHdrHeadroom - H[i+1]) / (H[i] - H[i+1]);
        result.fWeight[1] = 1.f - result.fWeight[0];
        result.fAlternateImageIndex[0] = indices[i];
        result.fAlternateImageIndex[1] = indices[i+1];
    }

    // The baseline image always has a gain of 0, so set the weight for the baseline image to 0.
    for (size_t i = 0; i < 2; ++i) {
        if (result.fAlternateImageIndex[i] == Weighting::kInvalidIndex) {
            result.fWeight[i] = 0;
        } else if (result.fWeight[i] == 0) {
            result.fAlternateImageIndex[i] = Weighting::kInvalidIndex;
        }
    }

    // Sort the weights so that the first weight is always greater.
    if (result.fWeight[1] > result.fWeight[0]) {
        std::swap(result.fWeight[0], result.fWeight[1]);
        std::swap(result.fAlternateImageIndex[0], result.fAlternateImageIndex[1]);
    }

    return result;
}

void ApplyGain(const AdaptiveGlobalToneMap::HeadroomAdaptiveToneMap& hatm,
               SkSpan<SkColor4f> colors,
               float targetedHdrHeadroom) {
    SkASSERT(Validate(hatm));

    // This function implements Formula (4). It creates special cases for one or both of the weights
    // being zero.
    const auto weighting = AgtmHelpers::ComputeWeighting(hatm, targetedHdrHeadroom);

    if (weighting.fWeight[0] == 0.f) {
        // If no weight is non-zero, then no gain will be applied. Leave the points unchanged.
        return;
    } else if (weighting.fWeight[1] == 0.f) {
        // Special case the case of there being only one weighted gain function.
        const auto& gain =
            hatm.fAlternateImages[weighting.fAlternateImageIndex[0]].fColorGainFunction;
        const float w = weighting.fWeight[0];
        for (auto& C : colors) {
            SkColor4f G = AgtmHelpers::EvaluateColorGainFunction(gain, C);
            C = {
                C.fR * std::exp2(w * G.fR),
                C.fG * std::exp2(w * G.fG),
                C.fB * std::exp2(w * G.fB),
                C.fA,
            };
        }
    } else {
        // The general case of two weighted gain functions.
        const auto& gain0 =
            hatm.fAlternateImages[weighting.fAlternateImageIndex[0]].fColorGainFunction;
        const float w0 = weighting.fWeight[0];
        const auto& gain1 =
            hatm.fAlternateImages[weighting.fAlternateImageIndex[1]].fColorGainFunction;
        const float w1 = weighting.fWeight[1];
        for (auto& C : colors) {
            SkColor4f G0 = AgtmHelpers::EvaluateColorGainFunction(gain0, C);
            SkColor4f G1 = AgtmHelpers::EvaluateColorGainFunction(gain1, C);
            C = {
                C.fR * std::exp2(w0 * G0.fR + w1 * G1.fR),
                C.fG * std::exp2(w0 * G0.fG + w1 * G1.fG),
                C.fB * std::exp2(w0 * G0.fB + w1 * G1.fB),
                C.fA,
            };
        }
    }
}

sk_sp<SkColorSpace> GetGainApplicationSpace(
        const AdaptiveGlobalToneMap::HeadroomAdaptiveToneMap& hatm) {
    skcms_Matrix3x3 toXYZD50;
    if (!hatm.fGainApplicationSpacePrimaries.toXYZD50(&toXYZD50)) {
        return nullptr;
    }
    return SkColorSpace::MakeRGB(SkNamedTransferFn::kLinear, toXYZD50);
}

sk_sp<SkColorFilter> MakeColorFilter(
        const AdaptiveGlobalToneMap::HeadroomAdaptiveToneMap& hatm,
        float targetedHdrHeadroom,
        float scaleFactor) {
    const auto weighting = ComputeWeighting(hatm, targetedHdrHeadroom);

    auto effect = agtm_runtime_effect();
    if (!effect) {
        return nullptr;
    }
    SkRuntimeShaderBuilder builder(effect);
    builder.uniform("scale_factor") = scaleFactor;
    for (size_t a = 0; a < 2; ++a) {
        const char* weight_str[2] = {"weight_i", "weight_j"};
        builder.uniform(weight_str[a]) = weighting.fWeight[a];

        if (weighting.fWeight[a] == 0.f) {
            continue;
        }
        const auto& gain = hatm.fAlternateImages[
            weighting.fAlternateImageIndex[a]].fColorGainFunction;

        const char* mix_rgbx_str[2] = {"mix_rgbx_i", "mix_rgbx_j"};
        builder.uniform(mix_rgbx_str[a]) = SkColor4f({
            gain.fComponentMixing.fRed,
            gain.fComponentMixing.fGreen,
            gain.fComponentMixing.fBlue,
            0.f,
        });

        const char* mix_Mmcx_str[2] = {"mix_Mmcx_i", "mix_Mmcx_j"};
        builder.uniform(mix_Mmcx_str[a]) = SkColor4f({
            gain.fComponentMixing.fMax,
            gain.fComponentMixing.fMin,
            gain.fComponentMixing.fComponent,
            0.f,
        });

        const char* curve_texcoord_y_str[2] = {"curve_texcoord_y_i", "curve_texcoord_y_j"};
        builder.uniform(curve_texcoord_y_str[a]) = (weighting.fAlternateImageIndex[a] + 0.5f);

        const char* curve_N_cp_str[2] = {"curve_N_cp_i", "curve_N_cp_j"};
        builder.uniform(curve_N_cp_str[a]) = static_cast<float>(
            gain.fGainCurve.fControlPoints.size());
    }

    if (auto gainCurvesXYM = MakeGainCurveXYMImage(hatm)) {
        builder.child("curve_xym") = gainCurvesXYM->makeRawShader(
            SkSamplingOptions(SkFilterMode::kNearest));
    }

    auto gainApplicationColorSpace = GetGainApplicationSpace(hatm);
    if (!gainApplicationColorSpace) {
        return nullptr;
    }

    auto filter = builder.makeColorFilter();
    SkASSERT(filter);
    return filter->makeWithWorkingColorSpace(gainApplicationColorSpace);
}

// Return the maximum luminance from CLLI, MDCV, or a default.
static float get_max_luminance(const Metadata& metadata) {
    if (metadata.getContentLightLevelInformation(nullptr)) {
        ContentLightLevelInformation clli;
        if (metadata.getContentLightLevelInformation(&clli) && clli.fMaxCLL > 0.f) {
            return clli.fMaxCLL;
        }
    }
    if (metadata.getMasteringDisplayColorVolume(nullptr)) {
        MasteringDisplayColorVolume mdcv;
        if (metadata.getMasteringDisplayColorVolume(&mdcv) &&
            mdcv.fMaximumDisplayMasteringLuminance > 0.f) {
            return mdcv.fMaximumDisplayMasteringLuminance;
        }
    }
    return 1000.f;
}

bool PopulateToneMapAgtmParams(const Metadata& metadata,
                               const SkColorSpace* inputColorSpace,
                               AdaptiveGlobalToneMap* outAgtm,
                               float* outScaleFactor) {
    // If `inputColorSpace` is HLG or PQ, find the HDR reference white value. When the shader
    // starts, this is the luminance that will have been mapped to 1.0. We will populate
    // `outScaleFactor` with a scale such that the AGTM HDR reference white luminance (if specified
    // will be mapped to 1.0).
    bool inputIsPqOrHlg = false;
    float inputPqOrHlgWhite = AdaptiveGlobalToneMap::kDefaultHdrReferenceWhite;
    if (inputColorSpace) {
        skcms_TransferFunction trfn;
        inputColorSpace->transferFn(&trfn);
        switch (skcms_TransferFunction_getType(&trfn)) {
            case skcms_TFType_PQ:
            case skcms_TFType_HLG:
                inputIsPqOrHlg = true;
                inputPqOrHlgWhite = trfn.a;
                break;
            default:
                break;
        }
    }

    AdaptiveGlobalToneMap agtm;
    auto& hatm = agtm.fHeadroomAdaptiveToneMap;
    bool hadAgtmMetadata = metadata.getAdaptiveGlobalToneMap(&agtm);

    // SDR content that does not specify an inverse tone mapping will not have a default tone
    // mapping added.
    if (!inputIsPqOrHlg) {
        if (!hadAgtmMetadata || !hatm.has_value()) {
            return false;
        }
    }

    // If no AGTM was specified, populate the HDR reference white from the input color space.
    if (!hadAgtmMetadata) {
        agtm.fHdrReferenceWhite = inputPqOrHlgWhite;
    }

    // If no tone mapping was specified, then use RWTMO with the baseline HDR headroom computed
    // from the CLLI and MDCV metadata.
    if (!hatm.has_value()) {
        hatm = {{
            .fBaselineHdrHeadroom = std::log2(
                std::max(get_max_luminance(metadata) / agtm.fHdrReferenceWhite, 1.f))
        }};
        AgtmHelpers::PopulateUsingRwtmo(hatm.value());
    }

    if (outAgtm) {
        *outAgtm = agtm;
    }
    if (outScaleFactor) {
        *outScaleFactor = inputIsPqOrHlg ? inputPqOrHlgWhite / agtm.fHdrReferenceWhite : 1.f;
    }
    return true;
}

bool Validate(const AdaptiveGlobalToneMap& agtm) {
    if (agtm.fHdrReferenceWhite < 0.f || agtm.fHdrReferenceWhite > 10000.f) {
        SkCodecPrintf("Agtm validation failed: HdrReferenceWhite invalid\n");
        return false;
    }
    if (agtm.fHeadroomAdaptiveToneMap.has_value()) {
        if (!Validate(agtm.fHeadroomAdaptiveToneMap.value())) {
            return false;
        }
    }
    return true;
}

bool Validate(const AdaptiveGlobalToneMap::HeadroomAdaptiveToneMap& hatm) {
    // Enforce the constraints listed in Clause 6.2.2.
    {
        // The value of H_baseline shall be in the interval [0, 6].
        if (hatm.fBaselineHdrHeadroom < kMinHdrHeadroom ||
            hatm.fBaselineHdrHeadroom > kMaxHdrHeadroom) {
            SkCodecPrintf("Agtm validation failed: H_baseline invalid\n");
            return false;
        }
        // The value of N_alt shall be in the set {0,1,2,3,4}.
        if (hatm.fAlternateImages.size() >
            AdaptiveGlobalToneMap::HeadroomAdaptiveToneMap::kMaxNumAlternateImages) {
            SkCodecPrintf("Agtm validation failed: NumAlternateImages invalid\n");
            return false;
        }
        // H_alt,i shall not equal H_baseline.
        for (const auto& alt : hatm.fAlternateImages) {
            if (alt.fHdrHeadroom == hatm.fBaselineHdrHeadroom) {
                SkCodecPrintf("Agtm validation failed: Alternate headroom equal to baseline\n");
                return false;
            }
        }
        // H_{alt,i} shall be strictly less than H_{alt,i+1}
        for (size_t a = 1; a < hatm.fAlternateImages.size(); ++a) {
            if (hatm.fAlternateImages[a].fHdrHeadroom <=
                hatm.fAlternateImages[a - 1].fHdrHeadroom) {
                SkCodecPrintf("Agtm validation failed: Alternate headrooms not ascending\n");
                return false;
            }
        }
        // Chromaticity coordinates must be in the unit interval.
        if (!in_unit_interval(hatm.fGainApplicationSpacePrimaries.fRX) ||
            !in_unit_interval(hatm.fGainApplicationSpacePrimaries.fRY) ||
            !in_unit_interval(hatm.fGainApplicationSpacePrimaries.fGX) ||
            !in_unit_interval(hatm.fGainApplicationSpacePrimaries.fGY) ||
            !in_unit_interval(hatm.fGainApplicationSpacePrimaries.fBX) ||
            !in_unit_interval(hatm.fGainApplicationSpacePrimaries.fBY) ||
            !in_unit_interval(hatm.fGainApplicationSpacePrimaries.fWX) ||
            !in_unit_interval(hatm.fGainApplicationSpacePrimaries.fWY)) {
            SkCodecPrintf("Agtm validation failed: chromaticities not in unit interval\n");
            return false;
        }
    }

    // Enforce the constraints listed in Clause 6.2.3 that are required for the metadata to be
    // encodable. The specification indicates that "implementations may adjust metadata items so
    // that they satisfy these constraints" for the other constraints.
    for (const auto& alt : hatm.fAlternateImages) {
        const auto& curve = alt.fColorGainFunction.fGainCurve;
        for (const auto& cp : curve.fControlPoints) {
            if (alt.fHdrHeadroom > hatm.fBaselineHdrHeadroom) {
                // If H_{alt,i} > H_baseline then GainCurve_i(x) >= 0.
                if (cp.fY < 0.f) {
                    SkCodecPrintf("Agtm validation failed: negative Y value in inverse tone map\n");
                    return false;
                }
            } else {
                // If H_{alt,i} < H_baseline then GainCurve_i(x) <= 0.
                if (cp.fY > 0.f) {
                    SkCodecPrintf("Agtm validation failed: positive Y value in tone map\n");
                    return false;
                }
            }
        }
    }

    // Enforce the constraints listed in Clause 6.4.2.
    for (const auto& alt : hatm.fAlternateImages) {
        const auto& mix = alt.fColorGainFunction.fComponentMixing;
        // All components shall be in the interval [0, 1].
        if (!in_unit_interval(mix.fRed) || !in_unit_interval(mix.fGreen) ||
            !in_unit_interval(mix.fBlue) || !in_unit_interval(mix.fMax) ||
            !in_unit_interval(mix.fMin) || !in_unit_interval(mix.fComponent)) {
            SkCodecPrintf("Agtm validation failed: mix coefficients not in unit interval\n");
            return false;
        }
        // The sum of all components shall equal 1. The coefficients are encoded in steps of
        // 0.00001. Allow the sum to be at most one step away from 1.
        const float sum = mix.fRed + mix.fGreen + mix.fBlue + mix.fMax + mix.fMin + mix.fComponent;
        if (sum < 0.99999f || sum > 1.00001f) {
            SkCodecPrintf("Agtm validation failed: mix coefficients don't sum to 1\n");
            return false;
        }
    }

    // Enforce the constraints listed in Clause 6.5.2.
    for (const auto& alt : hatm.fAlternateImages) {
        const auto& curve = alt.fColorGainFunction.fGainCurve;
        // The value of N_cp must be in the set {1,...,32}.
        if (curve.fControlPoints.size() < AdaptiveGlobalToneMap::GainCurve::kMinNumControlPoints ||
            curve.fControlPoints.size() > AdaptiveGlobalToneMap::GainCurve::kMaxNumControlPoints) {
            SkCodecPrintf("Agtm validation failed: invalid number of control points\n");
            return false;
        }
        for (size_t c = 0; c < curve.fControlPoints.size(); ++c) {
            // The value of x_i shall be in the interval [0, 64].
            if (curve.fControlPoints[c].fX < 0.f ||
                curve.fControlPoints[c].fX > kMaxLinearHdrHeadroom) {
                SkCodecPrintf("Agtm validation failed: invalid X value\n");
                return false;
            }
            // The value of y_i shall be in the interval [-6, 6].
            if (curve.fControlPoints[c].fY < -kMaxHdrHeadroom ||
                curve.fControlPoints[c].fY > kMaxHdrHeadroom) {
                SkCodecPrintf("Agtm validation failed: invalid Y value\n");
                return false;
            }
            if (c > 0) {
                // It shall be the case that x_i <= x_{i+1}.
                if (curve.fControlPoints[c].fX < curve.fControlPoints[c - 1].fX) {
                    SkCodecPrintf("Agtm validation failed: curve X is not non-decreasing\n");
                    return false;
                }
                // If x_i == x_{i+1} then it shall be the case that y_i == y_{i+1}.
                if (curve.fControlPoints[c].fX == curve.fControlPoints[c - 1].fX &&
                    curve.fControlPoints[c].fY != curve.fControlPoints[c - 1].fY) {
                    SkCodecPrintf("Agtm validation failed: curve has Y discontinuity\n");
                    return false;
                }
            }
        }
    }
    return true;
}

}  // namespace AgtmHelpers

SkString AdaptiveGlobalToneMap::toString() const {
    SkString result = SkStringPrintf("{hdrReferenceWhite:%f", fHdrReferenceWhite);
    if (!fHeadroomAdaptiveToneMap.has_value()) {
        result += "}";
        return result;
    }
    auto& hatm = fHeadroomAdaptiveToneMap.value();

    result += SkStringPrintf(", baselineHdrHeadroom:%f", hatm.fBaselineHdrHeadroom);
    result += ", alternateHdrHeadrooms:[";
    for (size_t a = 0; a < hatm.fAlternateImages.size(); ++a) {
        result += SkStringPrintf("%f", hatm.fAlternateImages[a].fHdrHeadroom);
        if (a != hatm.fAlternateImages.size() - 1) {
            result += ", ";
        }
    }
    result += "]}";
    return result;
}

}  // namespace skhdr

